authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-19 19:58:48-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-19 19:58:48-07:00
log1cf3647da62ee4c363f7c3d963c709dbddaa6297
treec72668448643d1c5b09cc68cfc988bdad862fcbf
parent53d0107759b743cdd7a9b6e777d4628650ed2131
signature Signed by PGP key B5690EEEBB952194

[codex] Normalize server core Effect service modules (#3187)

Co-authored-by: codex <codex@users.noreply.github.com>

25 files changed, 725 insertions(+), 708 deletions(-)

apps/server/src/auth/EnvironmentAuth.test.ts+10-9
......@@ -4,27 +4,26 @@ import { expect, it } from "@effect/vitest";
44import * as Effect from "effect/Effect";
55import * as Layer from "effect/Layer";
66
7import type { ServerConfigShape } from "../config.ts";
8import { ServerConfig } from "../config.ts";
7import * as ServerConfig from "../config.ts";
98import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";
109import * as PairingGrantStore from "./PairingGrantStore.ts";
1110import * as EnvironmentAuth from "./EnvironmentAuth.ts";
1211
1312import * as ServerSecretStore from "./ServerSecretStore.ts";
1413
15const makeServerConfigLayer = (overrides?: Partial<ServerConfigShape>) =>
14const makeServerConfigLayer = (overrides?: Partial<ServerConfig.ServerConfig["Service"]>) =>
1615 Layer.effect(
17 ServerConfig,
16 ServerConfig.ServerConfig,
1817 Effect.gen(function* () {
19 const config = yield* ServerConfig;
18 const config = yield* ServerConfig.ServerConfig;
2019 return {
2120 ...config,
2221 ...overrides,
23 } satisfies ServerConfigShape;
22 } satisfies ServerConfig.ServerConfig["Service"];
2423 }),
2524 ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-server-test-" })));
2625
27const makeEnvironmentAuthLayer = (overrides?: Partial<ServerConfigShape>) =>
26const makeEnvironmentAuthLayer = (overrides?: Partial<ServerConfig.ServerConfig["Service"]>) =>
2827 EnvironmentAuth.layer.pipe(
2928 Layer.provide(SqlitePersistenceMemory),
3029 Layer.provide(ServerSecretStore.layer),
......@@ -33,13 +32,15 @@ const makeEnvironmentAuthLayer = (overrides?: Partial<ServerConfigShape>) =>
3332
3433const makeCookieRequest = (
3534 sessionToken: string,
36): Parameters<EnvironmentAuth.EnvironmentAuthShape["authenticateHttpRequest"]>[0] =>
35): Parameters<EnvironmentAuth.EnvironmentAuth["Service"]["authenticateHttpRequest"]>[0] =>
3736 ({
3837 cookies: {
3938 t3_session: sessionToken,
4039 },
4140 headers: {},
42 }) as unknown as Parameters<EnvironmentAuth.EnvironmentAuthShape["authenticateHttpRequest"]>[0];
41 }) as unknown as Parameters<
42 EnvironmentAuth.EnvironmentAuth["Service"]["authenticateHttpRequest"]
43 >[0];
4344
4445const requestMetadata = {
4546 deviceType: "desktop" as const,
apps/server/src/auth/EnvironmentAuthAdmin.test.ts+6-7
......@@ -3,31 +3,30 @@ import { expect, it } from "@effect/vitest";
33import * as Effect from "effect/Effect";
44import * as Layer from "effect/Layer";
55
6import type { ServerConfigShape } from "../config.ts";
7import { ServerConfig } from "../config.ts";
6import * as ServerConfig from "../config.ts";
87import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";
98import * as EnvironmentAuth from "./EnvironmentAuth.ts";
109import * as ServerSecretStore from "./ServerSecretStore.ts";
1110import * as SessionStore from "./SessionStore.ts";
1211
1312const makeServerConfigLayer = (
14 overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,
13 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
1514) =>
1615 Layer.effect(
17 ServerConfig,
16 ServerConfig.ServerConfig,
1817 Effect.gen(function* () {
19 const config = yield* ServerConfig;
18 const config = yield* ServerConfig.ServerConfig;
2019 return {
2120 ...config,
2221 ...overrides,
23 } satisfies ServerConfigShape;
22 } satisfies ServerConfig.ServerConfig["Service"];
2423 }),
2524 ).pipe(
2625 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-control-plane-test-" })),
2726 );
2827
2928const makeEnvironmentAuthLayer = (
30 overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,
29 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
3130) =>
3231 EnvironmentAuth.layer.pipe(
3332 Layer.provideMerge(ServerSecretStore.layer),
apps/server/src/auth/EnvironmentAuthPolicy.test.ts+7-6
......@@ -3,21 +3,22 @@ import { expect, it } from "@effect/vitest";
33import * as Effect from "effect/Effect";
44import * as Layer from "effect/Layer";
55
6import type { ServerConfigShape } from "../config.ts";
7import { ServerConfig } from "../config.ts";
6import * as ServerConfig from "../config.ts";
87import * as EnvironmentAuthPolicy from "./EnvironmentAuthPolicy.ts";
98
10const makeEnvironmentAuthPolicyLayer = (overrides?: Partial<ServerConfigShape>) =>
9const makeEnvironmentAuthPolicyLayer = (
10 overrides?: Partial<ServerConfig.ServerConfig["Service"]>,
11) =>
1112 EnvironmentAuthPolicy.layer.pipe(
1213 Layer.provide(
1314 Layer.effect(
14 ServerConfig,
15 ServerConfig.ServerConfig,
1516 Effect.gen(function* () {
16 const config = yield* ServerConfig;
17 const config = yield* ServerConfig.ServerConfig;
1718 return {
1819 ...config,
1920 ...overrides,
20 } satisfies ServerConfigShape;
21 } satisfies ServerConfig.ServerConfig["Service"];
2122 }),
2223 ).pipe(
2324 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-policy-test-" })),
apps/server/src/auth/PairingGrantStore.test.ts+6-7
......@@ -5,29 +5,28 @@ import * as Effect from "effect/Effect";
55import * as Layer from "effect/Layer";
66import * as TestClock from "effect/testing/TestClock";
77
8import type { ServerConfigShape } from "../config.ts";
9import { ServerConfig } from "../config.ts";
8import * as ServerConfig from "../config.ts";
109import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";
1110import * as PairingGrantStore from "./PairingGrantStore.ts";
1211
1312const makeServerConfigLayer = (
14 overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,
13 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
1514) =>
1615 Layer.effect(
17 ServerConfig,
16 ServerConfig.ServerConfig,
1817 Effect.gen(function* () {
19 const config = yield* ServerConfig;
18 const config = yield* ServerConfig.ServerConfig;
2019 return {
2120 ...config,
2221 ...overrides,
23 } satisfies ServerConfigShape;
22 } satisfies ServerConfig.ServerConfig["Service"];
2423 }),
2524 ).pipe(
2625 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-bootstrap-test-" })),
2726 );
2827
2928const makePairingGrantStoreLayer = (
30 overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,
29 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
3130) =>
3231 PairingGrantStore.layer.pipe(
3332 Layer.provide(SqlitePersistenceMemory),
apps/server/src/auth/SessionStore.test.ts+8-9
......@@ -5,30 +5,29 @@ import * as Effect from "effect/Effect";
55import * as Layer from "effect/Layer";
66import * as TestClock from "effect/testing/TestClock";
77
8import type { ServerConfigShape } from "../config.ts";
9import { ServerConfig } from "../config.ts";
8import * as ServerConfig from "../config.ts";
109import { PersistenceSqlError } from "../persistence/Errors.ts";
1110import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";
12import { AuthSessionRepository } from "../persistence/Services/AuthSessions.ts";
11import * as AuthSessions from "../persistence/Services/AuthSessions.ts";
1312import * as SessionStore from "./SessionStore.ts";
1413import * as ServerSecretStore from "./ServerSecretStore.ts";
1514
1615const makeServerConfigLayer = (
17 overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,
16 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
1817) =>
1918 Layer.effect(
20 ServerConfig,
19 ServerConfig.ServerConfig,
2120 Effect.gen(function* () {
22 const config = yield* ServerConfig;
21 const config = yield* ServerConfig.ServerConfig;
2322 return {
2423 ...config,
2524 ...overrides,
26 } satisfies ServerConfigShape;
25 } satisfies ServerConfig.ServerConfig["Service"];
2726 }),
2827 ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-session-test-" })));
2928
3029const makeSessionStoreLayer = (
31 overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,
30 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
3231) =>
3332 SessionStore.layer.pipe(
3433 Layer.provide(SqlitePersistenceMemory),
......@@ -41,7 +40,7 @@ const repositoryFailure = new PersistenceSqlError({
4140 detail: "sqlite is unavailable",
4241});
4342
44const failingSessionLookupRepositoryLayer = Layer.succeed(AuthSessionRepository, {
43const failingSessionLookupRepositoryLayer = Layer.succeed(AuthSessions.AuthSessionRepository, {
4544 create: () => Effect.void,
4645 getById: () => Effect.fail(repositoryFailure),
4746 listActive: () => Effect.succeed([]),
apps/server/src/bin.test.ts+11-14
......@@ -20,8 +20,8 @@ import * as TestConsole from "effect/testing/TestConsole";
2020import { Command } from "effect/unstable/cli";
2121
2222import { cli, makeCli } from "./bin.ts";
23import { deriveServerPaths, ServerConfig, type ServerConfigShape } from "./config.ts";
24import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts";
23import * as ServerConfig from "./config.ts";
24import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts";
2525import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts";
2626import { orchestrationHttpApiLayer } from "./orchestration/http.ts";
2727import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";
......@@ -57,7 +57,7 @@ const captureStdout = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
5757
5858const makeCliTestServerConfig = (baseDir: string) =>
5959 Effect.gen(function* () {
60 const derivedPaths = yield* deriveServerPaths(baseDir, undefined);
60 const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, undefined);
6161 return {
6262 logLevel: "Info",
6363 traceMinLevel: "Info",
......@@ -84,26 +84,23 @@ const makeCliTestServerConfig = (baseDir: string) =>
8484 logWebSocketEvents: false,
8585 tailscaleServeEnabled: false,
8686 tailscaleServePort: 443,
87 } satisfies ServerConfigShape;
87 } satisfies ServerConfig.ServerConfig["Service"];
8888 });
8989
90const makeProjectPersistenceLayer = (config: ServerConfigShape) =>
90const makeProjectPersistenceLayer = (config: ServerConfig.ServerConfig["Service"]) =>
9191 Layer.mergeAll(
9292 OrchestrationLayerLive.pipe(
9393 Layer.provideMerge(RepositoryIdentityResolverLive),
9494 Layer.provideMerge(SqlitePersistenceLayerLive),
9595 ),
9696 WorkspacePathsLive,
97 ).pipe(
98 Layer.provideMerge(NodeServices.layer),
99 Layer.provide(Layer.succeed(ServerConfig, config)),
100 );
97 ).pipe(Layer.provideMerge(NodeServices.layer), Layer.provide(ServerConfig.layer(config)));
10198
10299const readPersistedSnapshot = (baseDir: string) =>
103100 Effect.gen(function* () {
104101 const config = yield* makeCliTestServerConfig(baseDir);
105102 return yield* Effect.gen(function* () {
106 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
103 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
107104 return yield* projectionSnapshotQuery.getSnapshot();
108105 }).pipe(Effect.provide(makeProjectPersistenceLayer(config)));
109106 });
......@@ -133,7 +130,7 @@ const withLiveProjectCliServer = <A, E, R>(baseDir: string, run: () => Effect.Ef
133130 }),
134131 ),
135132 Layer.provideMerge(NodeServices.layer),
136 Layer.provide(Layer.succeed(ServerConfig, config)),
133 Layer.provide(ServerConfig.layer(config)),
137134 );
138135
139136 return yield* Effect.scoped(
......@@ -238,7 +235,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {
238235 it.effect("logs in to headless connect without enabling access", () =>
239236 Effect.gen(function* () {
240237 const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-cloud-login-test-"));
241 const { secretsDir } = yield* deriveServerPaths(baseDir, undefined);
238 const { secretsDir } = yield* ServerConfig.deriveServerPaths(baseDir, undefined);
242239 mkdirSync(secretsDir, { recursive: true });
243240 writeFileSync(
244241 join(secretsDir, "cloud-cli-oauth-token.bin"),
......@@ -282,7 +279,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {
282279 it.effect("logs out of headless connect and removes the stored CLI authorization", () =>
283280 Effect.gen(function* () {
284281 const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-cloud-logout-test-"));
285 const { secretsDir } = yield* deriveServerPaths(baseDir, undefined);
282 const { secretsDir } = yield* ServerConfig.deriveServerPaths(baseDir, undefined);
286283 const tokenPath = join(secretsDir, "cloud-cli-oauth-token.bin");
287284 mkdirSync(secretsDir, { recursive: true });
288285 writeFileSync(tokenPath, "invalid persisted token");
......@@ -461,7 +458,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {
461458 "--base-dir",
462459 baseDir,
463460 ]);
464 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
461 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
465462 const readModel = yield* projectionSnapshotQuery.getSnapshot();
466463 const addedProject = readModel.projects.find(
467464 (project) => project.workspaceRoot === workspaceRoot && project.deletedAt === null,
apps/server/src/cli/auth.ts+3-3
......@@ -18,7 +18,7 @@ import {
1818 formatPairingCredentialList,
1919 formatSessionList,
2020} from "../cliAuthFormat.ts";
21import { ServerConfig } from "../config.ts";
21import * as ServerConfig from "../config.ts";
2222import {
2323 authLocationFlags,
2424 type CliAuthLocationFlags,
......@@ -28,7 +28,7 @@ import {
2828
2929const runWithEnvironmentAuth = <A, E>(
3030 flags: CliAuthLocationFlags,
31 run: (environmentAuth: EnvironmentAuth.EnvironmentAuthShape) => Effect.Effect<A, E>,
31 run: (environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"]) => Effect.Effect<A, E>,
3232 options?: {
3333 readonly quietLogs?: boolean;
3434 },
......@@ -43,7 +43,7 @@ const runWithEnvironmentAuth = <A, E>(
4343 }).pipe(
4444 Effect.provide(
4545 Layer.mergeAll(EnvironmentAuth.runtimeLayer).pipe(
46 Layer.provide(Layer.succeed(ServerConfig, config)),
46 Layer.provide(ServerConfig.layer(config)),
4747 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),
4848 ),
4949 ),
apps/server/src/cli/config.ts+12-20
......@@ -14,18 +14,10 @@ import * as SchemaTransformation from "effect/SchemaTransformation";
1414import { Argument, Flag } from "effect/unstable/cli";
1515
1616import { readBootstrapEnvelope } from "../bootstrap.ts";
17import {
18 DEFAULT_PORT,
19 deriveServerPaths,
20 ensureServerDirectories,
21 resolveStaticDir,
22 RuntimeMode,
23 type ServerConfigShape,
24 type StartupPresentation,
25} from "../config.ts";
17import * as ServerConfig from "../config.ts";
2618import { expandHomePath, resolveBaseDir } from "../os-jank.ts";
2719
28export const modeFlag = Flag.choice("mode", RuntimeMode.literals).pipe(
20export const modeFlag = Flag.choice("mode", ServerConfig.RuntimeMode.literals).pipe(
2921 Flag.withDescription("Runtime mode. `desktop` keeps loopback defaults unless overridden."),
3022 Flag.optional,
3123);
......@@ -104,7 +96,7 @@ const EnvServerConfig = Config.all({
10496 Config.withDefault(10_000),
10597 ),
10698 otlpServiceName: Config.string("T3CODE_OTLP_SERVICE_NAME").pipe(Config.withDefault("t3-server")),
107 mode: Config.schema(RuntimeMode, "T3CODE_MODE").pipe(
99 mode: Config.schema(ServerConfig.RuntimeMode, "T3CODE_MODE").pipe(
108100 Config.option,
109101 Config.map(Option.getOrUndefined),
110102 ),
......@@ -139,7 +131,7 @@ const EnvServerConfig = Config.all({
139131});
140132
141133export interface CliServerFlags {
142 readonly mode: Option.Option<RuntimeMode>;
134 readonly mode: Option.Option<ServerConfig.RuntimeMode>;
143135 readonly port: Option.Option<number>;
144136 readonly host: Option.Option<string>;
145137 readonly baseDir: Option.Option<string>;
......@@ -208,7 +200,7 @@ export const resolveServerConfig = (
208200 flags: CliServerFlags,
209201 cliLogLevel: Option.Option<LogLevel.LogLevel>,
210202 options?: {
211 readonly startupPresentation?: StartupPresentation;
203 readonly startupPresentation?: ServerConfig.StartupPresentation;
212204 readonly forceAutoBootstrapProjectFromCwd?: boolean;
213205 },
214206) =>
......@@ -238,7 +230,7 @@ export const resolveServerConfig = (
238230 : Option.none();
239231 const bootstrap = Option.getOrUndefined(bootstrapEnvelope);
240232
241 const mode: RuntimeMode = Option.getOrElse(
233 const mode: ServerConfig.RuntimeMode = Option.getOrElse(
242234 resolveOptionPrecedence(
243235 normalizedFlags.mode,
244236 Option.fromUndefinedOr(env.mode),
......@@ -257,9 +249,9 @@ export const resolveServerConfig = (
257249 onSome: (value) => Effect.succeed(value),
258250 onNone: () => {
259251 if (mode === "desktop") {
260 return Effect.succeed(DEFAULT_PORT);
252 return Effect.succeed(ServerConfig.DEFAULT_PORT);
261253 }
262 return findAvailablePort(DEFAULT_PORT);
254 return findAvailablePort(ServerConfig.DEFAULT_PORT);
263255 },
264256 },
265257 );
......@@ -279,8 +271,8 @@ export const resolveServerConfig = (
279271 const rawCwd = Option.getOrElse(normalizedFlags.cwd, () => process.cwd());
280272 const cwd = path.resolve(yield* expandHomePath(rawCwd.trim()));
281273 yield* fs.makeDirectory(cwd, { recursive: true });
282 const derivedPaths = yield* deriveServerPaths(baseDir, devUrl);
283 yield* ensureServerDirectories(derivedPaths);
274 const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, devUrl);
275 yield* ServerConfig.ensureServerDirectories(derivedPaths);
284276 const persistedObservabilitySettings = yield* loadPersistedObservabilitySettings(
285277 derivedPaths.settingsPath,
286278 );
......@@ -330,7 +322,7 @@ export const resolveServerConfig = (
330322 ),
331323 () => 443,
332324 );
333 const staticDir = devUrl ? undefined : yield* resolveStaticDir();
325 const staticDir = devUrl ? undefined : yield* ServerConfig.resolveStaticDir();
334326 const host = Option.getOrElse(
335327 resolveOptionPrecedence(
336328 normalizedFlags.host,
......@@ -341,7 +333,7 @@ export const resolveServerConfig = (
341333 );
342334 const logLevel = Option.getOrElse(cliLogLevel, () => env.logLevel);
343335
344 const config: ServerConfigShape = {
336 const config: ServerConfig.ServerConfig["Service"] = {
345337 logLevel,
346338 traceMinLevel: env.traceMinLevel,
347339 traceTimingEnabled: env.traceTimingEnabled,
apps/server/src/cli/connect.ts+9-9
......@@ -31,9 +31,9 @@ import * as CliTokenManager from "../cloud/CliTokenManager.ts";
3131import { CLOUD_LINKED_USER_ID, RELAY_URL_SECRET } from "../cloud/config.ts";
3232import { relayUrlConfig } from "../cloud/publicConfig.ts";
3333import { headlessRelayClientTracingLayer } from "../cloud/relayTracing.ts";
34import { ServerConfig } from "../config.ts";
34import * as ServerConfig from "../config.ts";
3535import { ServerEnvironmentLive } from "../environment/Layers/ServerEnvironment.ts";
36import { ServerEnvironment } from "../environment/Services/ServerEnvironment.ts";
36import * as ServerEnvironment from "../environment/Services/ServerEnvironment.ts";
3737import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts";
3838import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts";
3939
......@@ -145,7 +145,7 @@ const reportRelayClientInstallProgress = (event: RelayClientInstallProgressEvent
145145
146146export const acquireRelayClientForLink = Effect.fn("cloud.cli.acquire_relay_client_for_link")(
147147 function* <ConfirmError, ConfirmContext>(
148 relayClient: RelayClient.RelayClientShape,
148 relayClient: RelayClient.RelayClient["Service"],
149149 confirmInstall: (version: string) => Effect.Effect<boolean, ConfirmError, ConfirmContext>,
150150 reportProgress: (event: RelayClientInstallProgressEvent) => Effect.Effect<void>,
151151 ) {
......@@ -164,7 +164,7 @@ export const acquireRelayClientForLink = Effect.fn("cloud.cli.acquire_relay_clie
164164);
165165
166166const withCloudCliSessionToken = <A, E, R>(
167 environmentAuth: EnvironmentAuth.EnvironmentAuthShape,
167 environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"],
168168 run: (token: string) => Effect.Effect<A, E, R>,
169169) =>
170170 Effect.acquireUseRelease(
......@@ -183,7 +183,7 @@ type LiveCloudActionResult =
183183 | { readonly status: "failed"; readonly cause: unknown };
184184
185185const runLiveCloudUnlink = Effect.fn("cloud.cli.run_live_unlink")(function* () {
186 const config = yield* ServerConfig;
186 const config = yield* ServerConfig.ServerConfig;
187187 const runtimeState = yield* readPersistedServerRuntimeState(config.serverRuntimeStatePath);
188188 if (Option.isNone(runtimeState)) {
189189 return { status: "not-running" } satisfies LiveCloudActionResult;
......@@ -219,7 +219,7 @@ const unlinkRelayEnvironment = Effect.fn("cloud.cli.unlink_relay_environment")(f
219219 return { status: "not-authenticated" } satisfies RelayUnlinkResult;
220220 }
221221
222 const environment = yield* ServerEnvironment;
222 const environment = yield* ServerEnvironment.ServerEnvironment;
223223 const environmentId = yield* environment.getEnvironmentId;
224224 const relayUrl = yield* relayUrlConfig;
225225 const httpClient = yield* HttpClient.HttpClient;
......@@ -285,8 +285,8 @@ const runCloudCommand = <A, E>(
285285 | FileSystem.FileSystem
286286 | HttpClient.HttpClient
287287 | Prompt.Environment
288 | ServerConfig
289 | ServerEnvironment
288 | ServerConfig.ServerConfig
289 | ServerEnvironment.ServerEnvironment
290290 >,
291291 options?: {
292292 readonly quietLogs?: boolean;
......@@ -305,7 +305,7 @@ const runCloudCommand = <A, E>(
305305 headlessRelayClientTracingLayer,
306306 ).pipe(
307307 Layer.provideMerge(FetchHttpClient.layer),
308 Layer.provideMerge(Layer.succeed(ServerConfig, config)),
308 Layer.provideMerge(ServerConfig.layer(config)),
309309 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),
310310 );
311311 return yield* run.pipe(Effect.provide(runtimeLayer));
apps/server/src/cli/project.ts+21-14
......@@ -26,19 +26,19 @@ import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient";
2626
2727import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
2828
29import { ServerConfig, type ServerConfigShape } from "../config.ts";
30import { OrchestrationEngineService } from "../orchestration/Services/OrchestrationEngine.ts";
31import { ProjectionSnapshotQuery } from "../orchestration/Services/ProjectionSnapshotQuery.ts";
29import * as ServerConfig from "../config.ts";
30import * as OrchestrationEngine from "../orchestration/Services/OrchestrationEngine.ts";
31import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts";
3232import { OrchestrationLayerLive } from "../orchestration/runtimeLayer.ts";
3333import { layerConfig as SqlitePersistenceLayerLive } from "../persistence/Layers/Sqlite.ts";
3434import { RepositoryIdentityResolverLive } from "../project/Layers/RepositoryIdentityResolver.ts";
35import { getAutoBootstrapDefaultModelSelection } from "../serverRuntimeStartup.ts";
35import * as ServerRuntimeStartup from "../serverRuntimeStartup.ts";
3636import {
3737 clearPersistedServerRuntimeState,
3838 readPersistedServerRuntimeState,
3939} from "../serverRuntimeState.ts";
4040import { WorkspacePathsLive } from "../workspace/Layers/WorkspacePaths.ts";
41import { WorkspacePaths } from "../workspace/Services/WorkspacePaths.ts";
41import * as WorkspacePaths from "../workspace/Services/WorkspacePaths.ts";
4242import { type CliAuthLocationFlags, projectLocationFlags, resolveCliAuthConfig } from "./config.ts";
4343
4444type ProjectMutationTarget = {
......@@ -78,7 +78,7 @@ const ProjectCliRuntimeLive = Layer.mergeAll(
7878const PROJECT_CLI_LIVE_SERVER_TIMEOUT = Duration.seconds(1);
7979const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError);
8080const withProjectCliSessionToken = <A, E, R>(
81 environmentAuth: EnvironmentAuth.EnvironmentAuthShape,
81 environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"],
8282 run: (token: string) => Effect.Effect<A, E, R>,
8383) =>
8484 Effect.acquireUseRelease(
......@@ -123,7 +123,7 @@ const makeLiveServerClient = (origin: string) =>
123123const normalizeWorkspaceRootForProjectCommand = Effect.fn(
124124 "normalizeWorkspaceRootForProjectCommand",
125125)(function* (workspaceRoot: string) {
126 const workspacePaths = yield* WorkspacePaths;
126 const workspacePaths = yield* WorkspacePaths.WorkspacePaths;
127127 return yield* workspacePaths.normalizeWorkspaceRoot(workspaceRoot);
128128});
129129
......@@ -211,12 +211,15 @@ const dispatchLiveOrchestrationCommand = (
211211 }).pipe(withProjectCliLiveServerTimeout, Effect.catch(failLiveServerRequest));
212212
213213const getOfflineSnapshot = Effect.fn("getOfflineSnapshot")(function* () {
214 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
214 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
215215 return yield* projectionSnapshotQuery.getSnapshot();
216216});
217217
218218const tryResolveLiveProjectExecutionMode = Effect.fn("tryResolveLiveProjectExecutionMode")(
219 function* (environmentAuth: EnvironmentAuth.EnvironmentAuthShape, config: ServerConfigShape) {
219 function* (
220 environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"],
221 config: ServerConfig.ServerConfig["Service"],
222 ) {
220223 const runtimeState = yield* readPersistedServerRuntimeState(config.serverRuntimeStatePath);
221224 if (Option.isNone(runtimeState)) {
222225 return Option.none<{ readonly origin: string }>();
......@@ -251,7 +254,11 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (
251254 }) => Effect.Effect<
252255 string,
253256 Error,
254 Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path | WorkspacePaths
257 | Crypto.Crypto
258 | FileSystem.FileSystem
259 | HttpClient.HttpClient
260 | Path.Path
261 | WorkspacePaths.WorkspacePaths
255262 >,
256263) {
257264 const logLevel = yield* GlobalFlag.LogLevel;
......@@ -278,13 +285,13 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (
278285 }
279286
280287 const offlineRuntimeLayer = ProjectCliRuntimeLive.pipe(
281 Layer.provide(Layer.succeed(ServerConfig, config)),
288 Layer.provide(ServerConfig.layer(config)),
282289 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),
283290 );
284291
285292 return yield* Effect.gen(function* () {
286293 const snapshot = yield* getOfflineSnapshot();
287 const orchestrationEngine = yield* OrchestrationEngineService;
294 const orchestrationEngine = yield* OrchestrationEngine.OrchestrationEngineService;
288295 const output = yield* run({
289296 snapshot,
290297 dispatch: (command) => orchestrationEngine.dispatch(command),
......@@ -296,7 +303,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (
296303 Effect.provide(
297304 Layer.mergeAll(EnvironmentAuth.runtimeLayer, WorkspacePathsLive).pipe(
298305 Layer.provideMerge(FetchHttpClient.layer),
299 Layer.provide(Layer.succeed(ServerConfig, config)),
306 Layer.provide(ServerConfig.layer(config)),
300307 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),
301308 ),
302309 ),
......@@ -341,7 +348,7 @@ const projectAddCommand = Command.make("add", {
341348 projectId,
342349 title,
343350 workspaceRoot,
344 defaultModelSelection: getAutoBootstrapDefaultModelSelection(),
351 defaultModelSelection: ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(),
345352 createdAt: DateTime.formatIso(yield* DateTime.now),
346353 });
347354 return `Added project ${projectId} (${title}) at ${workspaceRoot}.`;
apps/server/src/config.ts+83-76
......@@ -6,13 +6,13 @@
66 *
77 * @module ServerConfig
88 */
9import * as Context from "effect/Context";
910import * as Effect from "effect/Effect";
1011import * as FileSystem from "effect/FileSystem";
1112import * as Layer from "effect/Layer";
1213import * as LogLevel from "effect/LogLevel";
1314import * as Path from "effect/Path";
1415import * as Schema from "effect/Schema";
15import * as Context from "effect/Context";
1616
1717export const DEFAULT_PORT = 3773;
1818
......@@ -46,38 +46,51 @@ export interface ServerDerivedPaths {
4646}
4747
4848/**
49 * ServerConfigShape - Process/runtime configuration required by the server.
49 * ServerConfig - Service tag for server runtime configuration.
5050 */
51export interface ServerConfigShape extends ServerDerivedPaths {
52 readonly logLevel: LogLevel.LogLevel;
53 readonly traceMinLevel: LogLevel.LogLevel;
54 readonly traceTimingEnabled: boolean;
55 readonly traceBatchWindowMs: number;
56 readonly traceMaxBytes: number;
57 readonly traceMaxFiles: number;
58 readonly otlpTracesUrl: string | undefined;
59 readonly otlpMetricsUrl: string | undefined;
60 readonly otlpExportIntervalMs: number;
61 readonly otlpServiceName: string;
62 readonly mode: RuntimeMode;
63 readonly port: number;
64 readonly host: string | undefined;
65 readonly cwd: string;
66 readonly baseDir: string;
67 readonly staticDir: string | undefined;
68 readonly devUrl: URL | undefined;
69 readonly noBrowser: boolean;
70 readonly startupPresentation: StartupPresentation;
71 readonly desktopBootstrapToken: string | undefined;
72 readonly autoBootstrapProjectFromCwd: boolean;
73 readonly logWebSocketEvents: boolean;
74 readonly tailscaleServeEnabled: boolean;
75 readonly tailscaleServePort: number;
51export class ServerConfig extends Context.Service<
52 ServerConfig,
53 ServerDerivedPaths & {
54 readonly logLevel: LogLevel.LogLevel;
55 readonly traceMinLevel: LogLevel.LogLevel;
56 readonly traceTimingEnabled: boolean;
57 readonly traceBatchWindowMs: number;
58 readonly traceMaxBytes: number;
59 readonly traceMaxFiles: number;
60 readonly otlpTracesUrl: string | undefined;
61 readonly otlpMetricsUrl: string | undefined;
62 readonly otlpExportIntervalMs: number;
63 readonly otlpServiceName: string;
64 readonly mode: RuntimeMode;
65 readonly port: number;
66 readonly host: string | undefined;
67 readonly cwd: string;
68 readonly baseDir: string;
69 readonly staticDir: string | undefined;
70 readonly devUrl: URL | undefined;
71 readonly noBrowser: boolean;
72 readonly startupPresentation: StartupPresentation;
73 readonly desktopBootstrapToken: string | undefined;
74 readonly autoBootstrapProjectFromCwd: boolean;
75 readonly logWebSocketEvents: boolean;
76 readonly tailscaleServeEnabled: boolean;
77 readonly tailscaleServePort: number;
78 }
79>()("t3/config/ServerConfig") {
80 /** @deprecated Import and use `layerTest` from this module. */
81 static readonly layerTest = (
82 cwd: string,
83 baseDirOrPrefix: string | { readonly prefix: string },
84 ) => layerTest(cwd, baseDirOrPrefix);
7685}
7786
87export const make = (config: ServerConfig["Service"]) => ServerConfig.of(config);
88
89export const layer = (config: ServerConfig["Service"]) => Layer.succeed(ServerConfig, make(config));
90
7891export const deriveServerPaths = Effect.fn(function* (
79 baseDir: ServerConfigShape["baseDir"],
80 devUrl: ServerConfigShape["devUrl"],
92 baseDir: ServerConfig["Service"]["baseDir"],
93 devUrl: ServerConfig["Service"]["devUrl"],
8194): Effect.fn.Return<ServerDerivedPaths, never, Path.Path> {
8295 const { join } = yield* Path.Path;
8396 const stateDir = join(baseDir, devUrl !== undefined ? "dev" : "userdata");
......@@ -129,56 +142,50 @@ export const ensureServerDirectories = Effect.fn(function* (derivedPaths: Server
129142 );
130143});
131144
132/**
133 * ServerConfig - Service tag for server runtime configuration.
134 */
135export class ServerConfig extends Context.Service<ServerConfig, ServerConfigShape>()(
136 "t3/config/ServerConfig",
145const makeTest = Effect.fn("ServerConfig.makeTest")(function* (
146 cwd: string,
147 baseDirOrPrefix: string | { readonly prefix: string },
137148) {
138 static readonly layerTest = (cwd: string, baseDirOrPrefix: string | { prefix: string }) =>
139 Layer.effect(
140 ServerConfig,
141 Effect.gen(function* () {
142 const devUrl = undefined;
149 const devUrl = undefined;
150 const fs = yield* FileSystem.FileSystem;
151 const baseDir =
152 typeof baseDirOrPrefix === "string"
153 ? baseDirOrPrefix
154 : yield* fs.makeTempDirectoryScoped({ prefix: baseDirOrPrefix.prefix });
155 const derivedPaths = yield* deriveServerPaths(baseDir, devUrl);
156 yield* ensureServerDirectories(derivedPaths);
143157
144 const fs = yield* FileSystem.FileSystem;
145 const baseDir =
146 typeof baseDirOrPrefix === "string"
147 ? baseDirOrPrefix
148 : yield* fs.makeTempDirectoryScoped({ prefix: baseDirOrPrefix.prefix });
149 const derivedPaths = yield* deriveServerPaths(baseDir, devUrl);
150 yield* ensureServerDirectories(derivedPaths);
158 return ServerConfig.of({
159 logLevel: "Error",
160 traceMinLevel: "Info",
161 traceTimingEnabled: true,
162 traceBatchWindowMs: 200,
163 traceMaxBytes: 10 * 1024 * 1024,
164 traceMaxFiles: 10,
165 otlpTracesUrl: undefined,
166 otlpMetricsUrl: undefined,
167 otlpExportIntervalMs: 10_000,
168 otlpServiceName: "t3-server",
169 cwd,
170 baseDir,
171 ...derivedPaths,
172 mode: "web",
173 autoBootstrapProjectFromCwd: false,
174 logWebSocketEvents: false,
175 tailscaleServeEnabled: false,
176 tailscaleServePort: 443,
177 port: 0,
178 host: undefined,
179 desktopBootstrapToken: undefined,
180 staticDir: undefined,
181 devUrl,
182 noBrowser: false,
183 startupPresentation: "browser",
184 });
185});
151186
152 return {
153 logLevel: "Error",
154 traceMinLevel: "Info",
155 traceTimingEnabled: true,
156 traceBatchWindowMs: 200,
157 traceMaxBytes: 10 * 1024 * 1024,
158 traceMaxFiles: 10,
159 otlpTracesUrl: undefined,
160 otlpMetricsUrl: undefined,
161 otlpExportIntervalMs: 10_000,
162 otlpServiceName: "t3-server",
163 cwd,
164 baseDir,
165 ...derivedPaths,
166 mode: "web",
167 autoBootstrapProjectFromCwd: false,
168 logWebSocketEvents: false,
169 tailscaleServeEnabled: false,
170 tailscaleServePort: 443,
171 port: 0,
172 host: undefined,
173 desktopBootstrapToken: undefined,
174 staticDir: undefined,
175 devUrl,
176 noBrowser: false,
177 startupPresentation: "browser",
178 } satisfies ServerConfigShape;
179 }),
180 );
181}
187export const layerTest = (cwd: string, baseDirOrPrefix: string | { readonly prefix: string }) =>
188 Layer.effect(ServerConfig, makeTest(cwd, baseDirOrPrefix));
182189
183190export const resolveStaticDir = Effect.fn(function* () {
184191 const { join, resolve } = yield* Path.Path;
apps/server/src/environment/Layers/ServerEnvironment.test.ts+8-10
......@@ -8,15 +8,15 @@ import * as FileSystem from "effect/FileSystem";
88import * as Layer from "effect/Layer";
99import * as PlatformError from "effect/PlatformError";
1010
11import { deriveServerPaths, ServerConfig, type ServerConfigShape } from "../../config.ts";
12import { ServerEnvironment } from "../Services/ServerEnvironment.ts";
11import * as ServerConfig from "../../config.ts";
12import * as ServerEnvironment from "../Services/ServerEnvironment.ts";
1313import { ServerEnvironmentLive } from "./ServerEnvironment.ts";
1414
1515const makeServerEnvironmentLayer = (baseDir: string) =>
1616 ServerEnvironmentLive.pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)));
1717
1818const makeServerConfig = Effect.fn(function* (baseDir: string) {
19 const derivedPaths = yield* deriveServerPaths(baseDir, undefined);
19 const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, undefined);
2020
2121 return {
2222 ...derivedPaths,
......@@ -44,7 +44,7 @@ const makeServerConfig = Effect.fn(function* (baseDir: string) {
4444 devUrl: undefined,
4545 noBrowser: false,
4646 startupPresentation: "browser",
47 } satisfies ServerConfigShape;
47 } satisfies ServerConfig.ServerConfig["Service"];
4848});
4949
5050it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
......@@ -56,11 +56,11 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
5656 });
5757
5858 const first = yield* Effect.gen(function* () {
59 const serverEnvironment = yield* ServerEnvironment;
59 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;
6060 return yield* serverEnvironment.getDescriptor;
6161 }).pipe(Effect.provide(makeServerEnvironmentLayer(baseDir)));
6262 const second = yield* Effect.gen(function* () {
63 const serverEnvironment = yield* ServerEnvironment;
63 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;
6464 return yield* serverEnvironment.getDescriptor;
6565 }).pipe(Effect.provide(makeServerEnvironmentLayer(baseDir)));
6666
......@@ -109,14 +109,12 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
109109 });
110110
111111 const exit = yield* Effect.gen(function* () {
112 const serverEnvironment = yield* ServerEnvironment;
112 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;
113113 return yield* serverEnvironment.getDescriptor;
114114 }).pipe(
115115 Effect.provide(
116116 ServerEnvironmentLive.pipe(
117 Layer.provide(
118 Layer.merge(Layer.succeed(ServerConfig, serverConfig), failingFileSystemLayer),
119 ),
117 Layer.provide(Layer.merge(ServerConfig.layer(serverConfig), failingFileSystemLayer)),
120118 ),
121119 ),
122120 Effect.exit,
apps/server/src/keybindings.test.ts+48-55
......@@ -9,28 +9,21 @@ import * as Layer from "effect/Layer";
99import * as Logger from "effect/Logger";
1010import * as Path from "effect/Path";
1111import * as Schema from "effect/Schema";
12import { ServerConfig } from "./config.ts";
13
14import {
15 DEFAULT_KEYBINDINGS,
16 Keybindings,
17 KeybindingsLive,
18 ResolvedKeybindingFromConfig,
19 compileResolvedKeybindingRule,
20 compileResolvedKeybindingsConfig,
21 parseKeybindingShortcut,
22} from "./keybindings.ts";
12import * as ServerConfig from "./config.ts";
13import * as Keybindings from "./keybindings.ts";
2314import { KeybindingsConfigError } from "@t3tools/contracts";
2415
2516const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig);
2617const encodeKeybindingsConfigJson = Schema.encodeEffect(KeybindingsConfigJson);
2718const decodeKeybindingsConfigJson = Schema.decodeUnknownEffect(KeybindingsConfigJson);
28const encodeResolvedKeybindingFromConfig = Schema.encodeEffect(ResolvedKeybindingFromConfig);
19const encodeResolvedKeybindingFromConfig = Schema.encodeEffect(
20 Keybindings.ResolvedKeybindingFromConfig,
21);
2922const decodeResolvedKeybindingFromConfigExit = Schema.decodeUnknownExit(
30 ResolvedKeybindingFromConfig,
23 Keybindings.ResolvedKeybindingFromConfig,
3124);
3225const makeKeybindingsLayer = () => {
33 return KeybindingsLive.pipe(
26 return Keybindings.layer.pipe(
3427 Layer.provideMerge(
3528 Layer.fresh(
3629 ServerConfig.layerTest(process.cwd(), {
......@@ -66,7 +59,7 @@ const readKeybindingsConfig = (configPath: string) =>
6659it.layer(NodeServices.layer)("keybindings", (it) => {
6760 it.effect("parses shortcuts including plus key", () =>
6861 Effect.sync(() => {
69 assert.deepEqual(parseKeybindingShortcut("mod+j"), {
62 assert.deepEqual(Keybindings.parseKeybindingShortcut("mod+j"), {
7063 key: "j",
7164 metaKey: false,
7265 ctrlKey: false,
......@@ -74,7 +67,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
7467 altKey: false,
7568 modKey: true,
7669 });
77 assert.deepEqual(parseKeybindingShortcut("mod++"), {
70 assert.deepEqual(Keybindings.parseKeybindingShortcut("mod++"), {
7871 key: "+",
7972 metaKey: false,
8073 ctrlKey: false,
......@@ -87,7 +80,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
8780
8881 it.effect("compiles valid rule with parsed when AST", () =>
8982 Effect.sync(() => {
90 const compiled = compileResolvedKeybindingRule({
83 const compiled = Keybindings.compileResolvedKeybindingRule({
9184 key: "mod+d",
9285 command: "terminal.split",
9386 when: "terminalOpen && !terminalFocus",
......@@ -137,14 +130,14 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
137130 it.effect("rejects invalid rules", () =>
138131 Effect.sync(() => {
139132 assert.isNull(
140 compileResolvedKeybindingRule({
133 Keybindings.compileResolvedKeybindingRule({
141134 key: "mod+shift+d+o",
142135 command: "terminal.new",
143136 }),
144137 );
145138
146139 assert.isNull(
147 compileResolvedKeybindingRule({
140 Keybindings.compileResolvedKeybindingRule({
148141 key: "mod+d",
149142 command: "terminal.split",
150143 when: "terminalFocus && (",
......@@ -152,7 +145,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
152145 );
153146
154147 assert.isNull(
155 compileResolvedKeybindingRule({
148 Keybindings.compileResolvedKeybindingRule({
156149 key: "mod+d",
157150 command: "terminal.split",
158151 when: `${"!".repeat(300)}terminalFocus`,
......@@ -181,23 +174,23 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
181174 it.effect("bootstraps default keybindings when config file is missing", () =>
182175 Effect.gen(function* () {
183176 const fs = yield* FileSystem.FileSystem;
184 const { keybindingsConfigPath } = yield* ServerConfig;
177 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
185178 assert.isFalse(yield* fs.exists(keybindingsConfigPath));
186179
187180 yield* Effect.gen(function* () {
188 const keybindings = yield* Keybindings;
181 const keybindings = yield* Keybindings.Keybindings;
189182 yield* keybindings.syncDefaultKeybindingsOnStartup;
190183 });
191184
192185 const persisted = yield* readKeybindingsConfig(keybindingsConfigPath);
193 assert.deepEqual(persisted, DEFAULT_KEYBINDINGS);
186 assert.deepEqual(persisted, Keybindings.DEFAULT_KEYBINDINGS);
194187 }).pipe(Effect.provide(makeKeybindingsLayer())),
195188 );
196189
197190 it.effect("ships configurable thread navigation defaults", () =>
198191 Effect.sync(() => {
199192 const defaultsByCommand = new Map(
200 DEFAULT_KEYBINDINGS.map((binding) => [binding.command, binding.key] as const),
193 Keybindings.DEFAULT_KEYBINDINGS.map((binding) => [binding.command, binding.key] as const),
201194 );
202195
203196 assert.equal(defaultsByCommand.get("thread.previous"), "mod+shift+[");
......@@ -215,17 +208,17 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
215208 it.effect("uses defaults in runtime when config is malformed without overriding file", () =>
216209 Effect.gen(function* () {
217210 const fs = yield* FileSystem.FileSystem;
218 const { keybindingsConfigPath } = yield* ServerConfig;
211 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
219212 yield* fs.writeFileString(keybindingsConfigPath, "{ not-json");
220213
221214 const configState = yield* Effect.gen(function* () {
222 const keybindings = yield* Keybindings;
215 const keybindings = yield* Keybindings.Keybindings;
223216 return yield* keybindings.loadConfigState;
224217 });
225218
226219 assert.deepEqual(
227220 configState.keybindings,
228 compileResolvedKeybindingsConfig(DEFAULT_KEYBINDINGS),
221 Keybindings.compileResolvedKeybindingsConfig(Keybindings.DEFAULT_KEYBINDINGS),
229222 );
230223 assert.deepEqual(configState.issues, [
231224 {
......@@ -240,7 +233,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
240233 it.effect("ignores invalid entries in runtime and reports them as issues", () =>
241234 Effect.gen(function* () {
242235 const fs = yield* FileSystem.FileSystem;
243 const { keybindingsConfigPath } = yield* ServerConfig;
236 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
244237 yield* fs.writeFileString(
245238 keybindingsConfigPath,
246239 // @effect-diagnostics-next-line preferSchemaOverJson:off
......@@ -252,7 +245,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
252245 );
253246
254247 const configState = yield* Effect.gen(function* () {
255 const keybindings = yield* Keybindings;
248 const keybindings = yield* Keybindings.Keybindings;
256249 return yield* keybindings.loadConfigState;
257250 });
258251
......@@ -279,14 +272,14 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
279272 "upserts missing default keybindings on startup without overriding existing command rules",
280273 () =>
281274 Effect.gen(function* () {
282 const { keybindingsConfigPath } = yield* ServerConfig;
275 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
283276 yield* writeKeybindingsConfig(keybindingsConfigPath, [
284277 { key: "mod+shift+t", command: "terminal.toggle" },
285278 { key: "mod+shift+r", command: "script.run-tests.run" },
286279 ]);
287280
288281 yield* Effect.gen(function* () {
289 const keybindings = yield* Keybindings;
282 const keybindings = yield* Keybindings.Keybindings;
290283 yield* keybindings.syncDefaultKeybindingsOnStartup;
291284 });
292285
......@@ -300,7 +293,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
300293 persisted.some((entry) => entry.command === "terminal.toggle" && entry.key === "mod+j"),
301294 );
302295
303 for (const defaultRule of DEFAULT_KEYBINDINGS) {
296 for (const defaultRule of Keybindings.DEFAULT_KEYBINDINGS) {
304297 assert.isTrue(byCommand.has(defaultRule.command), `expected ${defaultRule.command}`);
305298 }
306299 assert.isTrue(byCommand.has("script.run-tests.run"));
......@@ -314,13 +307,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
314307 });
315308
316309 return Effect.gen(function* () {
317 const { keybindingsConfigPath } = yield* ServerConfig;
310 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
318311 yield* writeKeybindingsConfig(keybindingsConfigPath, [
319312 { key: "mod+j", command: "script.custom-action.run" },
320313 ]);
321314
322315 yield* Effect.gen(function* () {
323 const keybindings = yield* Keybindings;
316 const keybindings = yield* Keybindings.Keybindings;
324317 yield* keybindings.syncDefaultKeybindingsOnStartup;
325318 });
326319
......@@ -345,13 +338,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
345338
346339 it.effect("upserts custom keybindings to configured path", () =>
347340 Effect.gen(function* () {
348 const { keybindingsConfigPath } = yield* ServerConfig;
341 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
349342 yield* writeKeybindingsConfig(keybindingsConfigPath, [
350343 { key: "mod+j", command: "terminal.toggle" },
351344 ]);
352345
353346 const resolved = yield* Effect.gen(function* () {
354 const keybindings = yield* Keybindings;
347 const keybindings = yield* Keybindings.Keybindings;
355348 return yield* keybindings.upsertKeybindingRule({
356349 key: "mod+shift+r",
357350 command: "script.run-tests.run",
......@@ -371,12 +364,12 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
371364
372365 it.effect("appends additional custom keybindings for the same command", () =>
373366 Effect.gen(function* () {
374 const { keybindingsConfigPath } = yield* ServerConfig;
367 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
375368 yield* writeKeybindingsConfig(keybindingsConfigPath, [
376369 { key: "mod+r", command: "script.run-tests.run" },
377370 ]);
378371 yield* Effect.gen(function* () {
379 const keybindings = yield* Keybindings;
372 const keybindings = yield* Keybindings.Keybindings;
380373 return yield* keybindings.upsertKeybindingRule({
381374 key: "mod+shift+r",
382375 command: "script.run-tests.run",
......@@ -394,13 +387,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
394387
395388 it.effect("replaces only the targeted custom keybinding", () =>
396389 Effect.gen(function* () {
397 const { keybindingsConfigPath } = yield* ServerConfig;
390 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
398391 yield* writeKeybindingsConfig(keybindingsConfigPath, [
399392 { key: "mod+r", command: "script.run-tests.run" },
400393 { key: "mod+shift+r", command: "script.run-tests.run" },
401394 ]);
402395 yield* Effect.gen(function* () {
403 const keybindings = yield* Keybindings;
396 const keybindings = yield* Keybindings.Keybindings;
404397 return yield* keybindings.upsertKeybindingRule({
405398 key: "mod+alt+r",
406399 command: "script.run-tests.run",
......@@ -419,13 +412,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
419412
420413 it.effect("removes only the targeted custom keybinding", () =>
421414 Effect.gen(function* () {
422 const { keybindingsConfigPath } = yield* ServerConfig;
415 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
423416 yield* writeKeybindingsConfig(keybindingsConfigPath, [
424417 { key: "mod+r", command: "script.run-tests.run" },
425418 { key: "mod+shift+r", command: "script.run-tests.run" },
426419 ]);
427420 yield* Effect.gen(function* () {
428 const keybindings = yield* Keybindings;
421 const keybindings = yield* Keybindings.Keybindings;
429422 return yield* keybindings.removeKeybindingRule({
430423 key: "mod+r",
431424 command: "script.run-tests.run",
......@@ -441,11 +434,11 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
441434 it.effect("refuses to overwrite malformed keybindings config", () =>
442435 Effect.gen(function* () {
443436 const fs = yield* FileSystem.FileSystem;
444 const { keybindingsConfigPath } = yield* ServerConfig;
437 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
445438 yield* fs.writeFileString(keybindingsConfigPath, "{ not-json");
446439
447440 const result = yield* Effect.gen(function* () {
448 const keybindings = yield* Keybindings;
441 const keybindings = yield* Keybindings.Keybindings;
449442 return yield* keybindings.upsertKeybindingRule({
450443 key: "mod+shift+r",
451444 command: "script.run-tests.run",
......@@ -461,14 +454,14 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
461454 it.effect("reports non-array config parse errors without duplicate prefix", () =>
462455 Effect.gen(function* () {
463456 const fs = yield* FileSystem.FileSystem;
464 const { keybindingsConfigPath } = yield* ServerConfig;
457 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
465458 yield* fs.writeFileString(
466459 keybindingsConfigPath,
467460 '{"key":"mod+j","command":"terminal.toggle"}',
468461 );
469462
470463 const firstResult = yield* Effect.gen(function* () {
471 const keybindings = yield* Keybindings;
464 const keybindings = yield* Keybindings.Keybindings;
472465 return yield* keybindings.upsertKeybindingRule({
473466 key: "mod+shift+r",
474467 command: "script.run-tests.run",
......@@ -477,7 +470,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
477470 assertFailure(firstResult, "expected JSON array");
478471
479472 const secondResult = yield* Effect.gen(function* () {
480 const keybindings = yield* Keybindings;
473 const keybindings = yield* Keybindings.Keybindings;
481474 return yield* keybindings.upsertKeybindingRule({
482475 key: "mod+shift+r",
483476 command: "script.run-tests.run",
......@@ -490,7 +483,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
490483 it.effect("fails when config directory is not writable", () =>
491484 Effect.gen(function* () {
492485 const fs = yield* FileSystem.FileSystem;
493 const { keybindingsConfigPath } = yield* ServerConfig;
486 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
494487 const { dirname } = yield* Path.Path;
495488 yield* writeKeybindingsConfig(keybindingsConfigPath, [
496489 { key: "mod+j", command: "terminal.toggle" },
......@@ -498,7 +491,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
498491 yield* fs.chmod(dirname(keybindingsConfigPath), 0o500);
499492
500493 const result = yield* Effect.gen(function* () {
501 const keybindings = yield* Keybindings;
494 const keybindings = yield* Keybindings.Keybindings;
502495 return yield* keybindings.upsertKeybindingRule({
503496 key: "mod+shift+r",
504497 command: "script.run-tests.run",
......@@ -516,13 +509,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
516509
517510 it.effect("caches loaded resolved config across repeated reads", () =>
518511 Effect.gen(function* () {
519 const { keybindingsConfigPath } = yield* ServerConfig;
512 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
520513 yield* writeKeybindingsConfig(keybindingsConfigPath, [
521514 { key: "mod+j", command: "terminal.toggle" },
522515 ]);
523516
524517 const [first, second] = yield* Effect.gen(function* () {
525 const keybindings = yield* Keybindings;
518 const keybindings = yield* Keybindings.Keybindings;
526519 const firstLoad = (yield* keybindings.loadConfigState).keybindings;
527520 const secondLoad = (yield* keybindings.loadConfigState).keybindings;
528521 return [firstLoad, secondLoad] as const;
......@@ -535,13 +528,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
535528
536529 it.effect("updates cached resolved config after upsert", () =>
537530 Effect.gen(function* () {
538 const { keybindingsConfigPath } = yield* ServerConfig;
531 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
539532 yield* writeKeybindingsConfig(keybindingsConfigPath, [
540533 { key: "mod+j", command: "terminal.toggle" },
541534 ]);
542535
543536 const loadedAfterUpsert = yield* Effect.gen(function* () {
544 const keybindings = yield* Keybindings;
537 const keybindings = yield* Keybindings.Keybindings;
545538 yield* keybindings.loadConfigState;
546539 yield* keybindings.upsertKeybindingRule({
547540 key: "mod+shift+r",
......@@ -557,7 +550,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
557550
558551 it.effect("serializes concurrent upserts to avoid lost updates", () =>
559552 Effect.gen(function* () {
560 const { keybindingsConfigPath } = yield* ServerConfig;
553 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
561554 yield* writeKeybindingsConfig(keybindingsConfigPath, []);
562555
563556 const commands = Array.from(
......@@ -565,7 +558,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
565558 (_, index): KeybindingCommand => `script.concurrent-${index}.run`,
566559 );
567560 yield* Effect.gen(function* () {
568 const keybindings = yield* Keybindings;
561 const keybindings = yield* Keybindings.Keybindings;
569562 yield* Effect.all(
570563 commands.map((command, index) =>
571564 keybindings.upsertKeybindingRule({
apps/server/src/keybindings.ts+63-67
......@@ -41,7 +41,7 @@ import * as Context from "effect/Context";
4141import * as Scope from "effect/Scope";
4242import * as Stream from "effect/Stream";
4343import * as Semaphore from "effect/Semaphore";
44import { ServerConfig } from "./config.ts";
44import * as ServerConfig from "./config.ts";
4545import { writeFileStringAtomically } from "./atomicWrite.ts";
4646import { fromJsonStringPretty, fromLenientJson } from "@t3tools/shared/schemaJson";
4747import {
......@@ -225,74 +225,70 @@ function mergeWithDefaultKeybindings(custom: ResolvedKeybindingsConfig): Resolve
225225 return merged.slice(-MAX_KEYBINDINGS_COUNT);
226226}
227227
228/**
229 * KeybindingsShape - Service API for keybinding configuration operations.
230 */
231export interface KeybindingsShape {
232 /**
233 * Start the keybindings runtime and attach file watching.
234 *
235 * Safe to call multiple times. The first successful call establishes the
236 * runtime; later calls await the same startup.
237 */
238 readonly start: Effect.Effect<void, KeybindingsConfigError>;
239
240 /**
241 * Await keybindings runtime readiness.
242 *
243 * Readiness means the config directory exists, the watcher is attached, the
244 * startup sync has completed, and the current snapshot has been loaded.
245 */
246 readonly ready: Effect.Effect<void, KeybindingsConfigError>;
247
248 /**
249 * Ensure the on-disk keybindings file exists and includes all default
250 * commands so newly-added defaults are backfilled on startup.
251 */
252 readonly syncDefaultKeybindingsOnStartup: Effect.Effect<void, KeybindingsConfigError>;
253
254 /**
255 * Load runtime keybindings state along with non-fatal configuration issues.
256 */
257 readonly loadConfigState: Effect.Effect<KeybindingsConfigState, KeybindingsConfigError>;
258
259 /**
260 * Read the latest keybindings snapshot from cache/disk.
261 */
262 readonly getSnapshot: Effect.Effect<KeybindingsConfigState, KeybindingsConfigError>;
263
264 /**
265 * Stream of keybindings config change events.
266 */
267 readonly streamChanges: Stream.Stream<KeybindingsChangeEvent>;
268
269 /**
270 * Upsert a keybinding rule and persist the resulting configuration.
271 *
272 * Writes config atomically and enforces the max rule count by truncating
273 * oldest entries when needed.
274 */
275 readonly upsertKeybindingRule: (
276 input: ServerUpsertKeybindingInput,
277 ) => Effect.Effect<ResolvedKeybindingsConfig, KeybindingsConfigError>;
278
279 /**
280 * Remove a single persisted keybinding rule by exact key/command/when match.
281 */
282 readonly removeKeybindingRule: (
283 input: ServerRemoveKeybindingInput,
284 ) => Effect.Effect<ResolvedKeybindingsConfig, KeybindingsConfigError>;
285}
286
287228/**
288229 * Keybindings - Service tag for keybinding configuration operations.
289230 */
290export class Keybindings extends Context.Service<Keybindings, KeybindingsShape>()(
291 "t3/keybindings",
292) {}
231export class Keybindings extends Context.Service<
232 Keybindings,
233 {
234 /**
235 * Start the keybindings runtime and attach file watching.
236 *
237 * Safe to call multiple times. The first successful call establishes the
238 * runtime; later calls await the same startup.
239 */
240 readonly start: Effect.Effect<void, KeybindingsConfigError>;
241
242 /**
243 * Await keybindings runtime readiness.
244 *
245 * Readiness means the config directory exists, the watcher is attached, the
246 * startup sync has completed, and the current snapshot has been loaded.
247 */
248 readonly ready: Effect.Effect<void, KeybindingsConfigError>;
249
250 /**
251 * Ensure the on-disk keybindings file exists and includes all default
252 * commands so newly-added defaults are backfilled on startup.
253 */
254 readonly syncDefaultKeybindingsOnStartup: Effect.Effect<void, KeybindingsConfigError>;
255
256 /**
257 * Load runtime keybindings state along with non-fatal configuration issues.
258 */
259 readonly loadConfigState: Effect.Effect<KeybindingsConfigState, KeybindingsConfigError>;
260
261 /**
262 * Read the latest keybindings snapshot from cache/disk.
263 */
264 readonly getSnapshot: Effect.Effect<KeybindingsConfigState, KeybindingsConfigError>;
265
266 /**
267 * Stream of keybindings config change events.
268 */
269 readonly streamChanges: Stream.Stream<KeybindingsChangeEvent>;
270
271 /**
272 * Upsert a keybinding rule and persist the resulting configuration.
273 *
274 * Writes config atomically and enforces the max rule count by truncating
275 * oldest entries when needed.
276 */
277 readonly upsertKeybindingRule: (
278 input: ServerUpsertKeybindingInput,
279 ) => Effect.Effect<ResolvedKeybindingsConfig, KeybindingsConfigError>;
280
281 /**
282 * Remove a single persisted keybinding rule by exact key/command/when match.
283 */
284 readonly removeKeybindingRule: (
285 input: ServerRemoveKeybindingInput,
286 ) => Effect.Effect<ResolvedKeybindingsConfig, KeybindingsConfigError>;
287 }
288>()("t3/keybindings") {}
293289
294const makeKeybindings = Effect.gen(function* () {
295 const { keybindingsConfigPath } = yield* ServerConfig;
290const make = Effect.gen(function* () {
291 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
296292 const fs = yield* FileSystem.FileSystem;
297293 const path = yield* Path.Path;
298294 const upsertSemaphore = yield* Semaphore.make(1);
......@@ -700,7 +696,7 @@ const makeKeybindings = Effect.gen(function* () {
700696 return nextResolved;
701697 }),
702698 ),
703 } satisfies KeybindingsShape;
699 } satisfies Keybindings["Service"];
704700});
705701
706export const KeybindingsLive = Layer.effect(Keybindings, makeKeybindings);
702export const layer = Layer.effect(Keybindings, make);
apps/server/src/provider/Layers/ProviderRegistry.test.ts+116-76
......@@ -32,8 +32,8 @@ import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
3232
3333import { checkCodexProviderStatus, type CodexAppServerProviderSnapshot } from "./CodexProvider.ts";
3434import { checkClaudeProviderStatus } from "./ClaudeProvider.ts";
35import { OpenCodeRuntimeLive } from "../opencodeRuntime.ts";
36import { NoOpProviderEventLoggers, ProviderEventLoggers } from "./ProviderEventLoggers.ts";
35import * as OpenCodeRuntime from "../opencodeRuntime.ts";
36import * as ProviderEventLoggers from "./ProviderEventLoggers.ts";
3737import { ProviderInstanceRegistryHydrationLive } from "./ProviderInstanceRegistryHydration.ts";
3838import {
3939 haveProvidersChanged,
......@@ -42,12 +42,12 @@ import {
4242 ProviderRegistryLive,
4343 selectProvidersByKind,
4444} from "./ProviderRegistry.ts";
45import { ServerConfig } from "../../config.ts";
46import { ServerSettingsService, type ServerSettingsShape } from "../../serverSettings.ts";
45import * as ServerConfig from "../../config.ts";
46import * as ServerSettingsModule from "../../serverSettings.ts";
4747import { readProviderStatusCache, resolveProviderStatusCachePath } from "../providerStatusCache.ts";
4848import type { ProviderInstance } from "../ProviderDriver.ts";
49import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts";
50import { ProviderRegistry } from "../Services/ProviderRegistry.ts";
49import * as ProviderInstanceRegistry from "../Services/ProviderInstanceRegistry.ts";
50import * as ProviderRegistry from "../Services/ProviderRegistry.ts";
5151import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts";
5252const decodeServerSettings = Schema.decodeSync(ServerSettings);
5353const encodeServerSettings = Schema.encodeSync(ServerSettings);
......@@ -294,11 +294,11 @@ function makeMutableServerSettingsService(
294294 get streamChanges() {
295295 return Stream.fromPubSub(changes);
296296 },
297 } satisfies ServerSettingsShape;
297 } satisfies ServerSettingsModule.ServerSettingsService["Service"];
298298 });
299299}
300300
301it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), TestHttpClientLive))(
301it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), TestHttpClientLive))(
302302 "ProviderRegistry",
303303 (it) => {
304304 describe("checkCodexProviderStatus", () => {
......@@ -636,14 +636,17 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
636636 adapter: {} as ProviderInstance["adapter"],
637637 textGeneration: {} as ProviderInstance["textGeneration"],
638638 } satisfies ProviderInstance;
639 const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, {
640 getInstance: (instanceId) =>
641 Effect.succeed(instanceId === codexInstanceId ? instance : undefined),
642 listInstances: Effect.succeed([instance]),
643 listUnavailable: Effect.succeed([]),
644 streamChanges: Stream.empty,
645 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),
646 });
639 const instanceRegistryLayer = Layer.succeed(
640 ProviderInstanceRegistry.ProviderInstanceRegistry,
641 {
642 getInstance: (instanceId) =>
643 Effect.succeed(instanceId === codexInstanceId ? instance : undefined),
644 listInstances: Effect.succeed([instance]),
645 listUnavailable: Effect.succeed([]),
646 streamChanges: Stream.empty,
647 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),
648 },
649 );
647650 const scope = yield* Scope.make();
648651 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
649652 const runtimeServices = yield* Layer.build(
......@@ -658,7 +661,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
658661 ),
659662 ).pipe(Scope.provide(scope));
660663 yield* Effect.gen(function* () {
661 const registry = yield* ProviderRegistry;
664 const registry = yield* ProviderRegistry.ProviderRegistry;
662665 assert.deepStrictEqual(yield* registry.getProviders, [initialProvider]);
663666 assert.strictEqual(yield* Ref.get(refreshCalls), 0);
664667 }).pipe(Effect.provide(runtimeServices));
......@@ -786,16 +789,19 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
786789 adapter: {} as ProviderInstance["adapter"],
787790 textGeneration: {} as ProviderInstance["textGeneration"],
788791 } satisfies ProviderInstance;
789 const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, {
790 getInstance: (instanceId) =>
791 Effect.succeed(instanceId === cursorInstanceId ? instance : undefined),
792 listInstances: Effect.succeed([instance]),
793 listUnavailable: Effect.succeed([]),
794 streamChanges: Stream.empty,
795 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>
796 PubSub.subscribe(pubsub),
797 ),
798 });
792 const instanceRegistryLayer = Layer.succeed(
793 ProviderInstanceRegistry.ProviderInstanceRegistry,
794 {
795 getInstance: (instanceId) =>
796 Effect.succeed(instanceId === cursorInstanceId ? instance : undefined),
797 listInstances: Effect.succeed([instance]),
798 listUnavailable: Effect.succeed([]),
799 streamChanges: Stream.empty,
800 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>
801 PubSub.subscribe(pubsub),
802 ),
803 },
804 );
799805 const scope = yield* Scope.make();
800806 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
801807 const runtimeServices = yield* Layer.build(
......@@ -811,8 +817,8 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
811817 ).pipe(Scope.provide(scope));
812818
813819 yield* Effect.gen(function* () {
814 const registry = yield* ProviderRegistry;
815 const config = yield* ServerConfig;
820 const registry = yield* ProviderRegistry.ProviderRegistry;
821 const config = yield* ServerConfig.ServerConfig;
816822 const filePath = yield* resolveProviderStatusCachePath({
817823 cacheDir: config.providerStatusCacheDir,
818824 instanceId: cursorInstanceId,
......@@ -880,16 +886,19 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
880886 adapter: {} as ProviderInstance["adapter"],
881887 textGeneration: {} as ProviderInstance["textGeneration"],
882888 } satisfies ProviderInstance;
883 const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, {
884 getInstance: (instanceId) =>
885 Effect.succeed(instanceId === codexInstanceId ? instance : undefined),
886 listInstances: Effect.succeed([instance]),
887 listUnavailable: Effect.succeed([]),
888 streamChanges: Stream.empty,
889 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>
890 PubSub.subscribe(pubsub),
891 ),
892 });
889 const instanceRegistryLayer = Layer.succeed(
890 ProviderInstanceRegistry.ProviderInstanceRegistry,
891 {
892 getInstance: (instanceId) =>
893 Effect.succeed(instanceId === codexInstanceId ? instance : undefined),
894 listInstances: Effect.succeed([instance]),
895 listUnavailable: Effect.succeed([]),
896 streamChanges: Stream.empty,
897 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>
898 PubSub.subscribe(pubsub),
899 ),
900 },
901 );
893902 const scope = yield* Scope.make();
894903 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
895904 const runtimeServices = yield* Layer.build(
......@@ -905,7 +914,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
905914 ).pipe(Scope.provide(scope));
906915
907916 yield* Effect.gen(function* () {
908 const registry = yield* ProviderRegistry;
917 const registry = yield* ProviderRegistry.ProviderRegistry;
909918
910919 assert.deepStrictEqual(yield* registry.getProviders, [cachedProvider]);
911920 assert.deepStrictEqual(yield* registry.refresh(codexDriver), [cachedProvider]);
......@@ -975,25 +984,28 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
975984 const instancesRef = yield* Ref.make<ReadonlyArray<ProviderInstance>>([codexInstance]);
976985 const failNextList = yield* Ref.make(false);
977986 const wait = () => Effect.yieldNow;
978 const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, {
979 getInstance: (instanceId) =>
980 Ref.get(instancesRef).pipe(
981 Effect.map((instances) =>
982 instances.find((instance) => instance.instanceId === instanceId),
987 const instanceRegistryLayer = Layer.succeed(
988 ProviderInstanceRegistry.ProviderInstanceRegistry,
989 {
990 getInstance: (instanceId) =>
991 Ref.get(instancesRef).pipe(
992 Effect.map((instances) =>
993 instances.find((instance) => instance.instanceId === instanceId),
994 ),
983995 ),
984 ),
985 listInstances: Effect.gen(function* () {
986 const shouldFail = yield* Ref.get(failNextList);
987 if (shouldFail) {
988 yield* Ref.set(failNextList, false);
989 return yield* Effect.die(new Error("simulated registry list failure"));
990 }
991 return yield* Ref.get(instancesRef);
992 }),
993 listUnavailable: Effect.succeed([]),
994 streamChanges: Stream.fromPubSub(changes),
995 subscribeChanges: PubSub.subscribe(changes),
996 });
996 listInstances: Effect.gen(function* () {
997 const shouldFail = yield* Ref.get(failNextList);
998 if (shouldFail) {
999 yield* Ref.set(failNextList, false);
1000 return yield* Effect.die(new Error("simulated registry list failure"));
1001 }
1002 return yield* Ref.get(instancesRef);
1003 }),
1004 listUnavailable: Effect.succeed([]),
1005 streamChanges: Stream.fromPubSub(changes),
1006 subscribeChanges: PubSub.subscribe(changes),
1007 },
1008 );
9971009 const scope = yield* Scope.make();
9981010 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
9991011 const runtimeServices = yield* Layer.build(
......@@ -1009,7 +1021,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
10091021 ).pipe(Scope.provide(scope));
10101022
10111023 yield* Effect.gen(function* () {
1012 const registry = yield* ProviderRegistry;
1024 const registry = yield* ProviderRegistry.ProviderRegistry;
10131025 assert.deepStrictEqual(yield* registry.getProviders, [codexProvider]);
10141026
10151027 yield* Ref.set(failNextList, true);
......@@ -1092,15 +1104,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
10921104 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
10931105 const providerRegistryLayer = ProviderRegistryLive.pipe(
10941106 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),
1095 Layer.provideMerge(Layer.succeed(ServerSettingsService, serverSettings)),
1107 Layer.provideMerge(
1108 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
1109 ),
10961110 Layer.provideMerge(
10971111 ServerConfig.layerTest(process.cwd(), {
10981112 prefix: "t3-provider-registry-",
10991113 }),
11001114 ),
11011115 Layer.provideMerge(TestHttpClientLive),
1102 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),
1103 Layer.provideMerge(OpenCodeRuntimeLive),
1116 Layer.provideMerge(
1117 Layer.succeed(
1118 ProviderEventLoggers.ProviderEventLoggers,
1119 ProviderEventLoggers.NoOpProviderEventLoggers,
1120 ),
1121 ),
1122 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
11041123 // NO spawner mock — `ChildProcessSpawner` is supplied by the
11051124 // outer `NodeServices.layer` on `it.layer(...)` and will
11061125 // genuinely spawn a subprocess. The missing-binary ENOENT is
......@@ -1112,7 +1131,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
11121131 );
11131132
11141133 yield* Effect.gen(function* () {
1115 const registry = yield* ProviderRegistry;
1134 const registry = yield* ProviderRegistry.ProviderRegistry;
11161135 let providers = yield* registry.getProviders;
11171136 for (
11181137 let attempts = 0;
......@@ -1177,15 +1196,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
11771196 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
11781197 const providerRegistryLayer = ProviderRegistryLive.pipe(
11791198 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),
1180 Layer.provideMerge(Layer.succeed(ServerSettingsService, serverSettings)),
1199 Layer.provideMerge(
1200 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
1201 ),
11811202 Layer.provideMerge(
11821203 ServerConfig.layerTest(process.cwd(), {
11831204 prefix: "t3-provider-registry-",
11841205 }),
11851206 ),
11861207 Layer.provideMerge(TestHttpClientLive),
1187 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),
1188 Layer.provideMerge(OpenCodeRuntimeLive),
1208 Layer.provideMerge(
1209 Layer.succeed(
1210 ProviderEventLoggers.ProviderEventLoggers,
1211 ProviderEventLoggers.NoOpProviderEventLoggers,
1212 ),
1213 ),
1214 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
11891215 Layer.updateService(ChildProcessSpawner.ChildProcessSpawner, (spawner) =>
11901216 ChildProcessSpawner.make((command) => {
11911217 spawnedCommands.push((command as { readonly command: string }).command);
......@@ -1199,7 +1225,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
11991225 );
12001226
12011227 yield* Effect.gen(function* () {
1202 const registry = yield* ProviderRegistry;
1228 const registry = yield* ProviderRegistry.ProviderRegistry;
12031229 // Boot-time probe: the default codex instance is enabled with
12041230 // `firstMissing`, so the real spawner yields ENOENT and the
12051231 // snapshot should be `status: "error"`.
......@@ -1291,15 +1317,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
12911317 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
12921318 const providerRegistryLayer = ProviderRegistryLive.pipe(
12931319 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),
1294 Layer.provideMerge(Layer.succeed(ServerSettingsService, serverSettings)),
1320 Layer.provideMerge(
1321 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
1322 ),
12951323 Layer.provideMerge(
12961324 ServerConfig.layerTest(process.cwd(), {
12971325 prefix: "t3-provider-registry-",
12981326 }),
12991327 ),
13001328 Layer.provideMerge(TestHttpClientLive),
1301 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),
1302 Layer.provideMerge(OpenCodeRuntimeLive),
1329 Layer.provideMerge(
1330 Layer.succeed(
1331 ProviderEventLoggers.ProviderEventLoggers,
1332 ProviderEventLoggers.NoOpProviderEventLoggers,
1333 ),
1334 ),
1335 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
13031336 Layer.provideMerge(NodeServices.layer),
13041337 );
13051338 const runtimeServices = yield* Layer.build(providerRegistryLayer).pipe(
......@@ -1307,7 +1340,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
13071340 );
13081341
13091342 yield* Effect.gen(function* () {
1310 const registry = yield* ProviderRegistry;
1343 const registry = yield* ProviderRegistry.ProviderRegistry;
13111344 const providers = yield* registry.getProviders;
13121345 const ghost = providers.find((provider) => provider.instanceId === "ghost_main");
13131346
......@@ -1345,15 +1378,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
13451378 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
13461379 const providerRegistryLayer = ProviderRegistryLive.pipe(
13471380 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),
1348 Layer.provideMerge(Layer.succeed(ServerSettingsService, serverSettings)),
1381 Layer.provideMerge(
1382 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
1383 ),
13491384 Layer.provideMerge(
13501385 ServerConfig.layerTest(process.cwd(), {
13511386 prefix: "t3-provider-registry-",
13521387 }),
13531388 ),
13541389 Layer.provideMerge(TestHttpClientLive),
1355 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),
1356 Layer.provideMerge(OpenCodeRuntimeLive),
1390 Layer.provideMerge(
1391 Layer.succeed(
1392 ProviderEventLoggers.ProviderEventLoggers,
1393 ProviderEventLoggers.NoOpProviderEventLoggers,
1394 ),
1395 ),
1396 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
13571397 Layer.provideMerge(
13581398 mockCommandSpawnerLayer((command, args) => {
13591399 if (command === "agent") {
......@@ -1380,13 +1420,13 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
13801420 );
13811421 const runtimeServices = yield* Layer.build(
13821422 Layer.mergeAll(
1383 Layer.succeed(ServerSettingsService, serverSettings),
1423 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
13841424 providerRegistryLayer,
13851425 ),
13861426 ).pipe(Scope.provide(scope));
13871427
13881428 yield* Effect.gen(function* () {
1389 const registry = yield* ProviderRegistry;
1429 const registry = yield* ProviderRegistry.ProviderRegistry;
13901430 const providers = yield* registry.getProviders;
13911431 const cursorProvider = providers.find(
13921432 (provider) => provider.instanceId === ProviderInstanceId.make("cursor"),
apps/server/src/provider/providerUpdateSettings.ts+2-2
......@@ -3,7 +3,7 @@ import * as Effect from "effect/Effect";
33import * as Equal from "effect/Equal";
44import * as Stream from "effect/Stream";
55
6import type { ServerSettingsShape } from "../serverSettings.ts";
6import type * as ServerSettingsModule from "../serverSettings.ts";
77
88export interface ProviderSnapshotSettings<Settings> {
99 readonly provider: Settings;
......@@ -29,7 +29,7 @@ export function haveProviderSnapshotSettingsChanged<Settings>(
2929
3030export function makeProviderSnapshotSettingsSource<Settings>(
3131 provider: Settings,
32 serverSettings: ServerSettingsShape,
32 serverSettings: ServerSettingsModule.ServerSettingsService["Service"],
3333): {
3434 readonly getSettings: Effect.Effect<ProviderSnapshotSettings<Settings>, ServerSettingsError>;
3535 readonly streamSettings: Stream.Stream<ProviderSnapshotSettings<Settings>>;
apps/server/src/server.test.ts+95-100
......@@ -69,56 +69,30 @@ import { vi } from "vite-plus/test";
6969
7070const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z");
7171
72import type { ServerConfigShape } from "./config.ts";
73import { deriveServerPaths, ServerConfig } from "./config.ts";
72import * as ServerConfig from "./config.ts";
7473import { makeRoutesLayer } from "./server.ts";
75import {
76 CheckpointDiffQuery,
77 type CheckpointDiffQueryShape,
78} from "./checkpointing/Services/CheckpointDiffQuery.ts";
79import { GitManager, type GitManagerShape } from "./git/GitManager.ts";
80import { Keybindings, type KeybindingsShape } from "./keybindings.ts";
74import * as CheckpointDiffQuery from "./checkpointing/Services/CheckpointDiffQuery.ts";
75import * as GitManager from "./git/GitManager.ts";
76import * as Keybindings from "./keybindings.ts";
8177import * as ExternalLauncher from "./process/externalLauncher.ts";
82import {
83 OrchestrationEngineService,
84 type OrchestrationEngineShape,
85} from "./orchestration/Services/OrchestrationEngine.ts";
78import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts";
8679import { OrchestrationListenerCallbackError } from "./orchestration/Errors.ts";
87import {
88 ProjectionSnapshotQuery,
89 type ProjectionSnapshotQueryShape,
90} from "./orchestration/Services/ProjectionSnapshotQuery.ts";
80import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts";
9181import { SqlitePersistenceMemory } from "./persistence/Layers/Sqlite.ts";
9282import { PersistenceSqlError } from "./persistence/Errors.ts";
93import {
94 ProviderRegistry,
95 type ProviderRegistryShape,
96} from "./provider/Services/ProviderRegistry.ts";
83import * as ProviderRegistry from "./provider/Services/ProviderRegistry.ts";
9784import { makeManualOnlyProviderMaintenanceCapabilities } from "./provider/providerMaintenance.ts";
98import { ServerLifecycleEvents, type ServerLifecycleEventsShape } from "./serverLifecycleEvents.ts";
99import { ServerRuntimeStartup, type ServerRuntimeStartupShape } from "./serverRuntimeStartup.ts";
100import { ServerSettingsService, type ServerSettingsShape } from "./serverSettings.ts";
101import { TerminalManager, type TerminalManagerShape } from "./terminal/Services/Manager.ts";
85import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";
86import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts";
87import * as ServerSettings from "./serverSettings.ts";
88import * as TerminalManager from "./terminal/Services/Manager.ts";
10289import * as PreviewManager from "./preview/Manager.ts";
10390import * as PortScanner from "./preview/PortScanner.ts";
104import {
105 BrowserTraceCollector,
106 type BrowserTraceCollectorShape,
107} from "./observability/Services/BrowserTraceCollector.ts";
91import * as BrowserTraceCollector from "./observability/Services/BrowserTraceCollector.ts";
10892import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts";
109import {
110 ProjectSetupScriptRunner,
111 ProjectSetupScriptRunnerError,
112 type ProjectSetupScriptRunnerShape,
113} from "./project/Services/ProjectSetupScriptRunner.ts";
114import {
115 RepositoryIdentityResolver,
116 type RepositoryIdentityResolverShape,
117} from "./project/Services/RepositoryIdentityResolver.ts";
118import {
119 ServerEnvironment,
120 type ServerEnvironmentShape,
121} from "./environment/Services/ServerEnvironment.ts";
93import * as ProjectSetupScriptRunner from "./project/Services/ProjectSetupScriptRunner.ts";
94import * as RepositoryIdentityResolver from "./project/Services/RepositoryIdentityResolver.ts";
95import * as ServerEnvironment from "./environment/Services/ServerEnvironment.ts";
12296import * as WorkspaceEntries from "./workspace/WorkspaceEntries.ts";
12397import { WorkspaceFileSystemLive } from "./workspace/Layers/WorkspaceFileSystem.ts";
12498import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths.ts";
......@@ -132,10 +106,7 @@ import * as ReviewService from "./review/ReviewService.ts";
132106import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts";
133107import * as ServerSecretStore from "./auth/ServerSecretStore.ts";
134108import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
135import {
136 CloudManagedEndpointRuntime,
137 type CloudManagedEndpointRuntimeShape,
138} from "./cloud/ManagedEndpointRuntime.ts";
109import * as CloudManagedEndpointRuntime from "./cloud/ManagedEndpointRuntime.ts";
139110import * as CloudCliTokenManager from "./cloud/CliTokenManager.ts";
140111import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts";
141112import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts";
......@@ -341,32 +312,40 @@ const makeBrowserOtlpPayload = (spanName: string) =>
341312 });
342313
343314const buildAppUnderTest = (options?: {
344 config?: Partial<ServerConfigShape>;
315 config?: Partial<ServerConfig.ServerConfig["Service"]>;
345316 layers?: {
346 keybindings?: Partial<KeybindingsShape>;
347 providerRegistry?: Partial<ProviderRegistryShape>;
348 serverSettings?: Partial<ServerSettingsShape>;
349 externalLauncher?: Partial<ExternalLauncher.ExternalLauncherShape>;
350 vcsDriver?: Partial<VcsDriver.VcsDriverShape>;
351 vcsDriverRegistry?: Partial<VcsDriverRegistry.VcsDriverRegistryShape>;
352 gitVcsDriver?: Partial<GitVcsDriver.GitVcsDriverShape>;
353 gitManager?: Partial<GitManagerShape>;
354 sourceControlRepositoryService?: Partial<SourceControlRepositoryService.SourceControlRepositoryServiceShape>;
355 reviewService?: Partial<ReviewService.ReviewServiceShape>;
356 vcsStatusBroadcaster?: Partial<VcsStatusBroadcaster.VcsStatusBroadcasterShape>;
357 projectSetupScriptRunner?: Partial<ProjectSetupScriptRunnerShape>;
358 terminalManager?: Partial<TerminalManagerShape>;
359 orchestrationEngine?: Partial<OrchestrationEngineShape>;
360 projectionSnapshotQuery?: Partial<ProjectionSnapshotQueryShape>;
361 checkpointDiffQuery?: Partial<CheckpointDiffQueryShape>;
362 browserTraceCollector?: Partial<BrowserTraceCollectorShape>;
363 serverLifecycleEvents?: Partial<ServerLifecycleEventsShape>;
364 serverRuntimeStartup?: Partial<ServerRuntimeStartupShape>;
365 serverEnvironment?: Partial<ServerEnvironmentShape>;
366 repositoryIdentityResolver?: Partial<RepositoryIdentityResolverShape>;
367 cloudManagedEndpointRuntime?: Partial<CloudManagedEndpointRuntimeShape>;
368 relayClient?: Partial<RelayClient.RelayClientShape>;
369 cloudCliTokenManager?: Partial<CloudCliTokenManager.CloudCliTokenManagerShape>;
317 keybindings?: Partial<Keybindings.Keybindings["Service"]>;
318 providerRegistry?: Partial<ProviderRegistry.ProviderRegistry["Service"]>;
319 serverSettings?: Partial<ServerSettings.ServerSettingsService["Service"]>;
320 externalLauncher?: Partial<ExternalLauncher.ExternalLauncher["Service"]>;
321 vcsDriver?: Partial<VcsDriver.VcsDriver["Service"]>;
322 vcsDriverRegistry?: Partial<VcsDriverRegistry.VcsDriverRegistry["Service"]>;
323 gitVcsDriver?: Partial<GitVcsDriver.GitVcsDriver["Service"]>;
324 gitManager?: Partial<GitManager.GitManager["Service"]>;
325 sourceControlRepositoryService?: Partial<
326 SourceControlRepositoryService.SourceControlRepositoryService["Service"]
327 >;
328 reviewService?: Partial<ReviewService.ReviewService["Service"]>;
329 vcsStatusBroadcaster?: Partial<VcsStatusBroadcaster.VcsStatusBroadcaster["Service"]>;
330 projectSetupScriptRunner?: Partial<
331 ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]
332 >;
333 terminalManager?: Partial<TerminalManager.TerminalManager["Service"]>;
334 orchestrationEngine?: Partial<OrchestrationEngine.OrchestrationEngineService["Service"]>;
335 projectionSnapshotQuery?: Partial<ProjectionSnapshotQuery.ProjectionSnapshotQuery["Service"]>;
336 checkpointDiffQuery?: Partial<CheckpointDiffQuery.CheckpointDiffQuery["Service"]>;
337 browserTraceCollector?: Partial<BrowserTraceCollector.BrowserTraceCollector["Service"]>;
338 serverLifecycleEvents?: Partial<ServerLifecycleEvents.ServerLifecycleEvents["Service"]>;
339 serverRuntimeStartup?: Partial<ServerRuntimeStartup.ServerRuntimeStartup["Service"]>;
340 serverEnvironment?: Partial<ServerEnvironment.ServerEnvironment["Service"]>;
341 repositoryIdentityResolver?: Partial<
342 RepositoryIdentityResolver.RepositoryIdentityResolver["Service"]
343 >;
344 cloudManagedEndpointRuntime?: Partial<
345 CloudManagedEndpointRuntime.CloudManagedEndpointRuntime["Service"]
346 >;
347 relayClient?: Partial<RelayClient.RelayClient["Service"]>;
348 cloudCliTokenManager?: Partial<CloudCliTokenManager.CloudCliTokenManager["Service"]>;
370349 };
371350}) =>
372351 Effect.gen(function* () {
......@@ -374,8 +353,8 @@ const buildAppUnderTest = (options?: {
374353 const tempBaseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-router-test-" });
375354 const baseDir = options?.config?.baseDir ?? tempBaseDir;
376355 const devUrl = options?.config?.devUrl;
377 const derivedPaths = yield* deriveServerPaths(baseDir, devUrl);
378 const config: ServerConfigShape = {
356 const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, devUrl);
357 const config: ServerConfig.ServerConfig["Service"] = {
379358 logLevel: "Info",
380359 traceMinLevel: "Info",
381360 traceTimingEnabled: true,
......@@ -403,8 +382,8 @@ const buildAppUnderTest = (options?: {
403382 tailscaleServePort: 443,
404383 ...options?.config,
405384 };
406 const layerConfig = Layer.succeed(ServerConfig, config);
407 const defaultVcsDriver: VcsDriver.VcsDriverShape = {
385 const layerConfig = ServerConfig.layer(config);
386 const defaultVcsDriver: VcsDriver.VcsDriver["Service"] = {
408387 capabilities: {
409388 kind: "git",
410389 supportsWorktrees: true,
......@@ -502,7 +481,7 @@ const buildAppUnderTest = (options?: {
502481 const gitVcsDriverLayer = Layer.mock(GitVcsDriver.GitVcsDriver)({
503482 ...options?.layers?.gitVcsDriver,
504483 });
505 const gitManagerLayer = Layer.mock(GitManager)({
484 const gitManagerLayer = Layer.mock(GitManager.GitManager)({
506485 ...options?.layers?.gitManager,
507486 });
508487 const workspaceEntriesLayer = WorkspaceEntries.layer.pipe(
......@@ -545,7 +524,7 @@ const buildAppUnderTest = (options?: {
545524 disableLogger: true,
546525 }).pipe(
547526 Layer.provide(
548 Layer.mock(Keybindings)({
527 Layer.mock(Keybindings.Keybindings)({
549528 loadConfigState: Effect.succeed({
550529 keybindings: [],
551530 issues: [],
......@@ -555,7 +534,7 @@ const buildAppUnderTest = (options?: {
555534 }),
556535 ),
557536 Layer.provide(
558 Layer.mock(ProviderRegistry)({
537 Layer.mock(ProviderRegistry.ProviderRegistry)({
559538 getProviders: Effect.succeed([]),
560539 refresh: () => Effect.succeed([]),
561540 refreshInstance: () => Effect.succeed([]),
......@@ -569,7 +548,7 @@ const buildAppUnderTest = (options?: {
569548 }),
570549 ),
571550 Layer.provide(
572 Layer.mock(ServerSettingsService)({
551 Layer.mock(ServerSettings.ServerSettingsService)({
573552 start: Effect.void,
574553 ready: Effect.void,
575554 getSettings: Effect.succeed(DEFAULT_SERVER_SETTINGS),
......@@ -658,13 +637,13 @@ const buildAppUnderTest = (options?: {
658637 ),
659638 Layer.provideMerge(vcsStatusBroadcasterLayer),
660639 Layer.provide(
661 Layer.mock(ProjectSetupScriptRunner)({
640 Layer.mock(ProjectSetupScriptRunner.ProjectSetupScriptRunner)({
662641 runForThread: () => Effect.succeed({ status: "no-script" as const }),
663642 ...options?.layers?.projectSetupScriptRunner,
664643 }),
665644 ),
666645 Layer.provide(
667 Layer.mock(TerminalManager)({
646 Layer.mock(TerminalManager.TerminalManager)({
668647 ...options?.layers?.terminalManager,
669648 }),
670649 ),
......@@ -692,7 +671,7 @@ const buildAppUnderTest = (options?: {
692671 ),
693672 ),
694673 Layer.provide(
695 Layer.mock(OrchestrationEngineService)({
674 Layer.mock(OrchestrationEngine.OrchestrationEngineService)({
696675 readEvents: () => Stream.empty,
697676 dispatch: () => Effect.succeed({ sequence: 0 }),
698677 streamDomainEvents: Stream.empty,
......@@ -700,7 +679,7 @@ const buildAppUnderTest = (options?: {
700679 }),
701680 ),
702681 Layer.provide(
703 Layer.mock(ProjectionSnapshotQuery)({
682 Layer.mock(ProjectionSnapshotQuery.ProjectionSnapshotQuery)({
704683 getCommandReadModel: () => Effect.succeed(makeDefaultOrchestrationReadModel()),
705684 getSnapshot: () => Effect.succeed(makeDefaultOrchestrationReadModel()),
706685 getShellSnapshot: () =>
......@@ -729,7 +708,7 @@ const buildAppUnderTest = (options?: {
729708 }),
730709 ),
731710 Layer.provide(
732 Layer.mock(CheckpointDiffQuery)({
711 Layer.mock(CheckpointDiffQuery.CheckpointDiffQuery)({
733712 getTurnDiff: () =>
734713 Effect.succeed({
735714 threadId: defaultThreadId,
......@@ -751,13 +730,13 @@ const buildAppUnderTest = (options?: {
751730
752731 const appLayer = servedRoutesLayer.pipe(
753732 Layer.provide(
754 Layer.mock(BrowserTraceCollector)({
733 Layer.mock(BrowserTraceCollector.BrowserTraceCollector)({
755734 record: () => Effect.void,
756735 ...options?.layers?.browserTraceCollector,
757736 }),
758737 ),
759738 Layer.provide(
760 Layer.mock(ServerLifecycleEvents)({
739 Layer.mock(ServerLifecycleEvents.ServerLifecycleEvents)({
761740 publish: (event) => Effect.succeed({ ...(event as any), sequence: 1 }),
762741 snapshot: Effect.succeed({ sequence: 0, events: [] }),
763742 stream: Stream.empty,
......@@ -765,7 +744,7 @@ const buildAppUnderTest = (options?: {
765744 }),
766745 ),
767746 Layer.provide(
768 Layer.mock(ServerRuntimeStartup)({
747 Layer.mock(ServerRuntimeStartup.ServerRuntimeStartup)({
769748 awaitCommandReady: Effect.void,
770749 markHttpListening: Effect.void,
771750 enqueueCommand: (effect) => effect,
......@@ -773,22 +752,22 @@ const buildAppUnderTest = (options?: {
773752 }),
774753 ),
775754 Layer.provide(
776 Layer.mock(ServerEnvironment)({
755 Layer.mock(ServerEnvironment.ServerEnvironment)({
777756 getEnvironmentId: Effect.succeed(testEnvironmentDescriptor.environmentId),
778757 getDescriptor: Effect.succeed(testEnvironmentDescriptor),
779758 ...options?.layers?.serverEnvironment,
780759 }),
781760 ),
782761 Layer.provide(
783 Layer.mock(RepositoryIdentityResolver)({
762 Layer.mock(RepositoryIdentityResolver.RepositoryIdentityResolver)({
784763 resolve: () => Effect.succeed(null),
785764 ...options?.layers?.repositoryIdentityResolver,
786765 }),
787766 ),
788767 Layer.provide(
789768 Layer.succeed(
790 CloudManagedEndpointRuntime,
791 CloudManagedEndpointRuntime.of({
769 CloudManagedEndpointRuntime.CloudManagedEndpointRuntime,
770 CloudManagedEndpointRuntime.CloudManagedEndpointRuntime.of({
792771 applyConfig: () => Effect.succeed({ status: "disabled" }),
793772 ...options?.layers?.cloudManagedEndpointRuntime,
794773 }),
......@@ -5938,14 +5917,14 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
59385917 }),
59395918 );
59405919 const fetchRemote = vi.fn(
5941 (_: Parameters<GitVcsDriver.GitVcsDriverShape["fetchRemote"]>[0]) =>
5920 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["fetchRemote"]>[0]) =>
59425921 Effect.sync(() => {
59435922 bootstrapGitOperations.push("fetch");
59445923 }),
59455924 );
59465925 const fetchedOriginCommit = "0123456789abcdef0123456789abcdef01234567";
59475926 const resolveRemoteTrackingCommit = vi.fn(
5948 (_: Parameters<GitVcsDriver.GitVcsDriverShape["resolveRemoteTrackingCommit"]>[0]) =>
5927 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["resolveRemoteTrackingCommit"]>[0]) =>
59495928 Effect.sync(() => {
59505929 bootstrapGitOperations.push("resolve-remote-commit");
59515930 return {
......@@ -5955,7 +5934,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
59555934 }),
59565935 );
59575936 const createWorktree = vi.fn(
5958 (_: Parameters<GitVcsDriver.GitVcsDriverShape["createWorktree"]>[0]) =>
5937 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["createWorktree"]>[0]) =>
59595938 Effect.sync(() => {
59605939 bootstrapGitOperations.push("create-worktree");
59615940 return {
......@@ -5967,7 +5946,11 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
59675946 }),
59685947 );
59695948 const runForThread = vi.fn(
5970 (_: Parameters<ProjectSetupScriptRunnerShape["runForThread"]>[0]) =>
5949 (
5950 _: Parameters<
5951 ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"]
5952 >[0],
5953 ) =>
59715954 Effect.succeed({
59725955 status: "started" as const,
59735956 scriptId: "setup",
......@@ -6101,7 +6084,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
61016084 Effect.gen(function* () {
61026085 const dispatchedCommands: Array<OrchestrationCommand> = [];
61036086 const createWorktree = vi.fn(
6104 (_: Parameters<GitVcsDriver.GitVcsDriverShape["createWorktree"]>[0]) =>
6087 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["createWorktree"]>[0]) =>
61056088 Effect.succeed({
61066089 worktree: {
61076090 refName: "t3code/bootstrap-refName",
......@@ -6110,8 +6093,16 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
61106093 }),
61116094 );
61126095 const runForThread = vi.fn(
6113 (_: Parameters<ProjectSetupScriptRunnerShape["runForThread"]>[0]) =>
6114 Effect.fail(new ProjectSetupScriptRunnerError({ message: "pty unavailable" })),
6096 (
6097 _: Parameters<
6098 ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"]
6099 >[0],
6100 ) =>
6101 Effect.fail(
6102 new ProjectSetupScriptRunner.ProjectSetupScriptRunnerError({
6103 message: "pty unavailable",
6104 }),
6105 ),
61156106 );
61166107
61176108 yield* buildAppUnderTest({
......@@ -6195,7 +6186,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
61956186 Effect.gen(function* () {
61966187 const dispatchedCommands: Array<OrchestrationCommand> = [];
61976188 const createWorktree = vi.fn(
6198 (_: Parameters<GitVcsDriver.GitVcsDriverShape["createWorktree"]>[0]) =>
6189 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["createWorktree"]>[0]) =>
61996190 Effect.succeed({
62006191 worktree: {
62016192 refName: "t3code/bootstrap-refName",
......@@ -6204,7 +6195,11 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
62046195 }),
62056196 );
62066197 const runForThread = vi.fn(
6207 (_: Parameters<ProjectSetupScriptRunnerShape["runForThread"]>[0]) =>
6198 (
6199 _: Parameters<
6200 ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"]
6201 >[0],
6202 ) =>
62086203 Effect.succeed({
62096204 status: "started" as const,
62106205 scriptId: "setup",
......@@ -6314,7 +6309,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
63146309 Effect.gen(function* () {
63156310 const dispatchedCommands: Array<OrchestrationCommand> = [];
63166311 const createWorktree = vi.fn(
6317 (_: Parameters<GitVcsDriver.GitVcsDriverShape["createWorktree"]>[0]) =>
6312 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["createWorktree"]>[0]) =>
63186313 Effect.die(new Error("worktree exploded")),
63196314 );
63206315
apps/server/src/server.ts+17-17
......@@ -4,7 +4,7 @@ import * as Layer from "effect/Layer";
44import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http";
55import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";
66
7import { ServerConfig } from "./config.ts";
7import * as ServerConfig from "./config.ts";
88import {
99 otlpTracesProxyRouteLayer,
1010 assetRouteLayer,
......@@ -16,15 +16,15 @@ import { fixPath } from "./os-jank.ts";
1616import { websocketRpcRouteLayer } from "./ws.ts";
1717import * as ExternalLauncher from "./process/externalLauncher.ts";
1818import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";
19import { ServerLifecycleEventsLive } from "./serverLifecycleEvents.ts";
19import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";
2020import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService.ts";
2121import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory.ts";
2222import { ProviderSessionRuntimeRepositoryLive } from "./persistence/Layers/ProviderSessionRuntime.ts";
2323import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry.ts";
24import { ProviderEventLoggersLive } from "./provider/Layers/ProviderEventLoggers.ts";
24import * as ProviderEventLoggers from "./provider/Layers/ProviderEventLoggers.ts";
2525import { ProviderServiceLive } from "./provider/Layers/ProviderService.ts";
2626import { ProviderSessionReaperLive } from "./provider/Layers/ProviderSessionReaper.ts";
27import { OpenCodeRuntimeLive } from "./provider/opencodeRuntime.ts";
27import * as OpenCodeRuntime from "./provider/opencodeRuntime.ts";
2828import { CheckpointDiffQueryLive } from "./checkpointing/Layers/CheckpointDiffQuery.ts";
2929import { CheckpointStoreLive } from "./checkpointing/Layers/CheckpointStore.ts";
3030import * as AzureDevOpsCli from "./sourceControl/AzureDevOpsCli.ts";
......@@ -40,8 +40,8 @@ import * as PreviewManager from "./preview/Manager.ts";
4040import * as PortScanner from "./preview/PortScanner.ts";
4141import * as ProcessRunner from "./processRunner.ts";
4242import * as GitManager from "./git/GitManager.ts";
43import { KeybindingsLive } from "./keybindings.ts";
44import { ServerRuntimeStartup, ServerRuntimeStartupLive } from "./serverRuntimeStartup.ts";
43import * as Keybindings from "./keybindings.ts";
44import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts";
4545import { OrchestrationReactorLive } from "./orchestration/Layers/OrchestrationReactor.ts";
4646import { RuntimeReceiptBusLive } from "./orchestration/Layers/RuntimeReceiptBus.ts";
4747import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion.ts";
......@@ -51,7 +51,7 @@ import { ThreadDeletionReactorLive } from "./orchestration/Layers/ThreadDeletion
5151import * as AgentAwarenessRelay from "./relay/AgentAwarenessRelay.ts";
5252import { hasCloudPublicConfig } from "./cloud/publicConfig.ts";
5353import { ProviderRegistryLive } from "./provider/Layers/ProviderRegistry.ts";
54import { ServerSettingsLive } from "./serverSettings.ts";
54import * as ServerSettings from "./serverSettings.ts";
5555import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts";
5656import { RepositoryIdentityResolverLive } from "./project/Layers/RepositoryIdentityResolver.ts";
5757import * as WorkspaceEntries from "./workspace/WorkspaceEntries.ts";
......@@ -112,14 +112,14 @@ const PtyAdapterLive = Layer.unwrap(
112112
113113const RelayClientLive = Layer.unwrap(
114114 Effect.gen(function* () {
115 const config = yield* ServerConfig;
115 const config = yield* ServerConfig.ServerConfig;
116116 return RelayClient.layerCloudflared({ baseDir: config.baseDir });
117117 }),
118118);
119119
120120const HttpServerLive = Layer.unwrap(
121121 Effect.gen(function* () {
122 const config = yield* ServerConfig;
122 const config = yield* ServerConfig.ServerConfig;
123123 if (typeof Bun !== "undefined") {
124124 const BunHttpServer = yield* Effect.promise(
125125 () => import("@effect/platform-bun/BunHttpServer"),
......@@ -292,7 +292,7 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(
292292 Layer.provideMerge(ProviderRuntimeLayerLive),
293293 Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive)),
294294 Layer.provideMerge(PersistenceLayerLive),
295 Layer.provideMerge(KeybindingsLive),
295 Layer.provideMerge(Keybindings.layer),
296296 Layer.provideMerge(ProviderRegistryLive),
297297 // The instance registry is the new routing keystone — text generation,
298298 // adapter lookup, and runtime ingestion all resolve `ProviderInstanceId`
......@@ -305,14 +305,14 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(
305305 // `ProviderService` (canonical stream, written after event normalization).
306306 // Provided once at the runtime level so every consumer sees the same
307307 // logger instances.
308 Layer.provideMerge(ProviderEventLoggersLive),
308 Layer.provideMerge(ProviderEventLoggers.ProviderEventLoggersLive),
309309 // `OpenCodeDriver.create()` yields `OpenCodeRuntime`; previously the old
310310 // `ProviderRegistryLive` pulled `OpenCodeRuntimeLive` in for itself, but
311311 // the rewritten registry reads snapshots off the instance registry and
312312 // no longer transitively provides it. Exposing it at the runtime level
313313 // keeps a single Live for all opencode consumers.
314 Layer.provideMerge(OpenCodeRuntimeLive),
315 Layer.provideMerge(ServerSettingsLive),
314 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
315 Layer.provideMerge(ServerSettings.layer.pipe(Layer.provide(ServerSecretStore.layer))),
316316 Layer.provideMerge(WorkspaceLayerLive),
317317 Layer.provideMerge(ProjectFaviconResolverLayerLive),
318318 Layer.provideMerge(RepositoryIdentityResolverLive),
......@@ -334,11 +334,11 @@ const RuntimeDependenciesLive = RuntimeCoreDependenciesLive.pipe(
334334 Layer.provideMerge(TraceDiagnostics.layer),
335335 Layer.provideMerge(AnalyticsServiceLayerLive),
336336 Layer.provideMerge(ExternalLauncher.layer),
337 Layer.provideMerge(ServerLifecycleEventsLive),
337 Layer.provideMerge(ServerLifecycleEvents.layer),
338338 Layer.provide(NetService.layer),
339339);
340340
341const RuntimeServicesLive = ServerRuntimeStartupLive.pipe(
341const RuntimeServicesLive = ServerRuntimeStartup.layer.pipe(
342342 Layer.provideMerge(RuntimeDependenciesLive),
343343);
344344
......@@ -361,14 +361,14 @@ export const makeRoutesLayer = Layer.mergeAll(
361361
362362export const makeServerLayer = Layer.unwrap(
363363 Effect.gen(function* () {
364 const config = yield* ServerConfig;
364 const config = yield* ServerConfig.ServerConfig;
365365
366366 yield* fixPath();
367367
368368 const httpListeningLayer = Layer.effectDiscard(
369369 Effect.gen(function* () {
370370 yield* HttpServer.HttpServer;
371 const startup = yield* ServerRuntimeStartup;
371 const startup = yield* ServerRuntimeStartup.ServerRuntimeStartup;
372372 yield* startup.markHttpListening;
373373 }),
374374 );
apps/server/src/serverLifecycleEvents.test.ts+3-3
......@@ -4,13 +4,13 @@ import { assertTrue } from "@effect/vitest/utils";
44import * as Effect from "effect/Effect";
55import * as Option from "effect/Option";
66
7import { ServerLifecycleEvents, ServerLifecycleEventsLive } from "./serverLifecycleEvents.ts";
7import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";
88
99it.effect(
1010 "publishes lifecycle events without subscribers and snapshots the latest welcome/ready",
1111 () =>
1212 Effect.gen(function* () {
13 const lifecycleEvents = yield* ServerLifecycleEvents;
13 const lifecycleEvents = yield* ServerLifecycleEvents.ServerLifecycleEvents;
1414 const environment = {
1515 environmentId: EnvironmentId.make("environment-test"),
1616 label: "Test environment",
......@@ -49,5 +49,5 @@ it.effect(
4949 const snapshot = yield* lifecycleEvents.snapshot;
5050 assert.equal(snapshot.sequence, 2);
5151 assert.deepEqual(snapshot.events.map((event) => event.type).toSorted(), ["ready", "welcome"]);
52 }).pipe(Effect.provide(ServerLifecycleEventsLive)),
52 }).pipe(Effect.provide(ServerLifecycleEvents.layer)),
5353);
apps/server/src/serverLifecycleEvents.ts+34-37
......@@ -1,9 +1,9 @@
11import type { ServerLifecycleStreamEvent } from "@t3tools/contracts";
2import * as Context from "effect/Context";
23import * as Effect from "effect/Effect";
34import * as Layer from "effect/Layer";
45import * as PubSub from "effect/PubSub";
56import * as Ref from "effect/Ref";
6import * as Context from "effect/Context";
77import * as Stream from "effect/Stream";
88
99type LifecycleEventInput =
......@@ -15,44 +15,41 @@ interface SnapshotState {
1515 readonly events: ReadonlyArray<ServerLifecycleStreamEvent>;
1616}
1717
18export interface ServerLifecycleEventsShape {
19 readonly publish: (event: LifecycleEventInput) => Effect.Effect<ServerLifecycleStreamEvent>;
20 readonly snapshot: Effect.Effect<SnapshotState>;
21 readonly stream: Stream.Stream<ServerLifecycleStreamEvent>;
22}
23
2418export class ServerLifecycleEvents extends Context.Service<
2519 ServerLifecycleEvents,
26 ServerLifecycleEventsShape
20 {
21 readonly publish: (event: LifecycleEventInput) => Effect.Effect<ServerLifecycleStreamEvent>;
22 readonly snapshot: Effect.Effect<SnapshotState>;
23 readonly stream: Stream.Stream<ServerLifecycleStreamEvent>;
24 }
2725>()("t3/serverLifecycleEvents") {}
2826
29export const ServerLifecycleEventsLive = Layer.effect(
30 ServerLifecycleEvents,
31 Effect.gen(function* () {
32 const pubsub = yield* PubSub.unbounded<ServerLifecycleStreamEvent>();
33 const state = yield* Ref.make<SnapshotState>({
34 sequence: 0,
35 events: [],
36 });
27const make = Effect.gen(function* () {
28 const pubsub = yield* PubSub.unbounded<ServerLifecycleStreamEvent>();
29 const state = yield* Ref.make<SnapshotState>({
30 sequence: 0,
31 events: [],
32 });
33
34 return {
35 publish: (event) =>
36 Ref.modify(state, (current) => {
37 const nextSequence = current.sequence + 1;
38 const nextEvent = {
39 ...event,
40 sequence: nextSequence,
41 } satisfies ServerLifecycleStreamEvent;
42 const nextEvents =
43 nextEvent.type === "welcome"
44 ? [nextEvent, ...current.events.filter((entry) => entry.type !== "welcome")]
45 : [nextEvent, ...current.events.filter((entry) => entry.type !== "ready")];
46 return [nextEvent, { sequence: nextSequence, events: nextEvents }] as const;
47 }).pipe(Effect.tap((event) => PubSub.publish(pubsub, event))),
48 snapshot: Ref.get(state),
49 get stream() {
50 return Stream.fromPubSub(pubsub);
51 },
52 } satisfies ServerLifecycleEvents["Service"];
53});
3754
38 return {
39 publish: (event) =>
40 Ref.modify(state, (current) => {
41 const nextSequence = current.sequence + 1;
42 const nextEvent = {
43 ...event,
44 sequence: nextSequence,
45 } satisfies ServerLifecycleStreamEvent;
46 const nextEvents =
47 nextEvent.type === "welcome"
48 ? [nextEvent, ...current.events.filter((entry) => entry.type !== "welcome")]
49 : [nextEvent, ...current.events.filter((entry) => entry.type !== "ready")];
50 return [nextEvent, { sequence: nextSequence, events: nextEvents }] as const;
51 }).pipe(Effect.tap((event) => PubSub.publish(pubsub, event))),
52 snapshot: Ref.get(state),
53 get stream() {
54 return Stream.fromPubSub(pubsub);
55 },
56 } satisfies ServerLifecycleEventsShape;
57 }),
58);
55export const layer = Layer.effect(ServerLifecycleEvents, make);
apps/server/src/serverRuntimeStartup.test.ts+32-42
......@@ -10,24 +10,14 @@ import * as PlatformError from "effect/PlatformError";
1010import * as Ref from "effect/Ref";
1111import * as Stream from "effect/Stream";
1212
13import { ServerConfig } from "./config.ts";
14import {
15 OrchestrationEngineService,
16 type OrchestrationEngineShape,
17} from "./orchestration/Services/OrchestrationEngine.ts";
18import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts";
19import { AnalyticsService } from "./telemetry/Services/AnalyticsService.ts";
20import {
21 getAutoBootstrapDefaultModelSelection,
22 launchStartupHeartbeat,
23 makeCommandGate,
24 resolveAutoBootstrapWelcomeTargets,
25 resolveWelcomeBase,
26 ServerRuntimeStartupError,
27} from "./serverRuntimeStartup.ts";
13import * as ServerConfig from "./config.ts";
14import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts";
15import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts";
16import * as AnalyticsService from "./telemetry/Services/AnalyticsService.ts";
17import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts";
2818
2919it("uses the canonical Codex default for auto-bootstrapped model selection", () => {
30 assert.deepStrictEqual(getAutoBootstrapDefaultModelSelection(), {
20 assert.deepStrictEqual(ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(), {
3121 instanceId: ProviderInstanceId.make("codex"),
3222 model: DEFAULT_MODEL,
3323 });
......@@ -37,7 +27,7 @@ it.effect("enqueueCommand waits for readiness and then drains queued work", () =
3727 Effect.scoped(
3828 Effect.gen(function* () {
3929 const executionCount = yield* Ref.make(0);
40 const commandGate = yield* makeCommandGate;
30 const commandGate = yield* ServerRuntimeStartup.makeCommandGate;
4131
4232 const queuedCommandFiber = yield* commandGate
4333 .enqueueCommand(Ref.updateAndGet(executionCount, (count) => count + 1))
......@@ -58,7 +48,7 @@ it.effect("enqueueCommand waits for readiness and then drains queued work", () =
5848it.effect("enqueueCommand fails queued work when readiness fails", () =>
5949 Effect.scoped(
6050 Effect.gen(function* () {
61 const commandGate = yield* makeCommandGate;
51 const commandGate = yield* ServerRuntimeStartup.makeCommandGate;
6252 const failure = yield* Deferred.make<void, never>();
6353
6454 const queuedCommandFiber = yield* commandGate
......@@ -66,13 +56,13 @@ it.effect("enqueueCommand fails queued work when readiness fails", () =>
6656 .pipe(Effect.forkScoped);
6757
6858 yield* commandGate.failCommandReady(
69 new ServerRuntimeStartupError({
70 message: "startup failed",
59 new ServerRuntimeStartup.ServerRuntimeStartupError({
60 stage: "command-readiness",
7161 }),
7262 );
7363
7464 const error = yield* Effect.flip(Fiber.join(queuedCommandFiber));
75 assert.equal(error.message, "startup failed");
65 assert.equal(error.message, "Server runtime startup failed before command readiness.");
7666 }),
7767 ),
7868);
......@@ -82,8 +72,8 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa
8272 Effect.gen(function* () {
8373 const releaseCounts = yield* Deferred.make<void, never>();
8474
85 yield* launchStartupHeartbeat.pipe(
86 Effect.provideService(ProjectionSnapshotQuery, {
75 yield* ServerRuntimeStartup.launchStartupHeartbeat.pipe(
76 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
8777 getCommandReadModel: () => Effect.die("unused"),
8878 getSnapshot: () => Effect.die("unused"),
8979 getShellSnapshot: () => Effect.die("unused"),
......@@ -104,7 +94,7 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa
10494 getThreadShellById: () => Effect.succeed(Option.none()),
10595 getThreadDetailById: () => Effect.succeed(Option.none()),
10696 }),
107 Effect.provideService(AnalyticsService, {
97 Effect.provideService(AnalyticsService.AnalyticsService, {
10898 record: () => Effect.void,
10999 flush: Effect.void,
110100 }),
......@@ -115,8 +105,8 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa
115105
116106it.effect("resolveWelcomeBase derives cwd and project name from server config", () =>
117107 Effect.gen(function* () {
118 const welcome = yield* resolveWelcomeBase.pipe(
119 Effect.provideService(ServerConfig, {
108 const welcome = yield* ServerRuntimeStartup.resolveWelcomeBase.pipe(
109 Effect.provideService(ServerConfig.ServerConfig, {
120110 cwd: "/tmp/startup-project",
121111 } as never),
122112 );
......@@ -134,12 +124,12 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa
134124
135125 return Effect.gen(function* () {
136126 const dispatchCalls = yield* Ref.make<ReadonlyArray<string>>([]);
137 const targets = yield* resolveAutoBootstrapWelcomeTargets.pipe(
138 Effect.provideService(ServerConfig, {
127 const targets = yield* ServerRuntimeStartup.resolveAutoBootstrapWelcomeTargets.pipe(
128 Effect.provideService(ServerConfig.ServerConfig, {
139129 cwd: "/tmp/startup-project",
140130 autoBootstrapProjectFromCwd: true,
141131 } as never),
142 Effect.provideService(ProjectionSnapshotQuery, {
132 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
143133 getCommandReadModel: () => Effect.die("unused"),
144134 getSnapshot: () => Effect.die("unused"),
145135 getShellSnapshot: () => Effect.die("unused"),
......@@ -152,7 +142,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa
152142 id: bootstrapProjectId,
153143 title: "Startup Project",
154144 workspaceRoot: "/tmp/startup-project",
155 defaultModelSelection: getAutoBootstrapDefaultModelSelection(),
145 defaultModelSelection: ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(),
156146 scripts: [],
157147 createdAt: "2026-01-01T00:00:00.000Z",
158148 updatedAt: "2026-01-01T00:00:00.000Z",
......@@ -166,14 +156,14 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa
166156 getThreadShellById: () => Effect.die("unused"),
167157 getThreadDetailById: () => Effect.die("unused"),
168158 }),
169 Effect.provideService(OrchestrationEngineService, {
159 Effect.provideService(OrchestrationEngine.OrchestrationEngineService, {
170160 readEvents: () => Stream.empty,
171161 dispatch: (command) =>
172162 Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe(
173163 Effect.as({ sequence: 1 }),
174164 ),
175165 streamDomainEvents: Stream.empty,
176 } satisfies OrchestrationEngineShape),
166 } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]),
177167 Effect.provide(NodeServices.layer),
178168 );
179169
......@@ -188,12 +178,12 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa
188178it.effect("resolveAutoBootstrapWelcomeTargets creates a project and thread when missing", () =>
189179 Effect.gen(function* () {
190180 const dispatchCalls = yield* Ref.make<ReadonlyArray<string>>([]);
191 const targets = yield* resolveAutoBootstrapWelcomeTargets.pipe(
192 Effect.provideService(ServerConfig, {
181 const targets = yield* ServerRuntimeStartup.resolveAutoBootstrapWelcomeTargets.pipe(
182 Effect.provideService(ServerConfig.ServerConfig, {
193183 cwd: "/tmp/startup-project",
194184 autoBootstrapProjectFromCwd: true,
195185 } as never),
196 Effect.provideService(ProjectionSnapshotQuery, {
186 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
197187 getCommandReadModel: () => Effect.die("unused"),
198188 getSnapshot: () => Effect.die("unused"),
199189 getShellSnapshot: () => Effect.die("unused"),
......@@ -208,14 +198,14 @@ it.effect("resolveAutoBootstrapWelcomeTargets creates a project and thread when
208198 getThreadShellById: () => Effect.die("unused"),
209199 getThreadDetailById: () => Effect.die("unused"),
210200 }),
211 Effect.provideService(OrchestrationEngineService, {
201 Effect.provideService(OrchestrationEngine.OrchestrationEngineService, {
212202 readEvents: () => Stream.empty,
213203 dispatch: (command) =>
214204 Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe(
215205 Effect.as({ sequence: 1 }),
216206 ),
217207 streamDomainEvents: Stream.empty,
218 } satisfies OrchestrationEngineShape),
208 } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]),
219209 Effect.provide(NodeServices.layer),
220210 );
221211
......@@ -236,12 +226,12 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa
236226 });
237227 const dispatchCalls = yield* Ref.make<ReadonlyArray<string>>([]);
238228
239 const error = yield* resolveAutoBootstrapWelcomeTargets.pipe(
240 Effect.provideService(ServerConfig, {
229 const error = yield* ServerRuntimeStartup.resolveAutoBootstrapWelcomeTargets.pipe(
230 Effect.provideService(ServerConfig.ServerConfig, {
241231 cwd: "/tmp/startup-project",
242232 autoBootstrapProjectFromCwd: true,
243233 } as never),
244 Effect.provideService(ProjectionSnapshotQuery, {
234 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
245235 getCommandReadModel: () => Effect.die("unused"),
246236 getSnapshot: () => Effect.die("unused"),
247237 getShellSnapshot: () => Effect.die("unused"),
......@@ -256,14 +246,14 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa
256246 getThreadShellById: () => Effect.die("unused"),
257247 getThreadDetailById: () => Effect.die("unused"),
258248 }),
259 Effect.provideService(OrchestrationEngineService, {
249 Effect.provideService(OrchestrationEngine.OrchestrationEngineService, {
260250 readEvents: () => Stream.empty,
261251 dispatch: (command) =>
262252 Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe(
263253 Effect.as({ sequence: 1 }),
264254 ),
265255 streamDomainEvents: Stream.empty,
266 } satisfies OrchestrationEngineShape),
256 } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]),
267257 Effect.provideService(Crypto.Crypto, {
268258 ...crypto,
269259 randomUUIDv4: Effect.fail(uuidError),
apps/server/src/serverRuntimeStartup.ts+53-48
......@@ -7,8 +7,10 @@ import {
77 ProviderInstanceId,
88 ThreadId,
99} from "@t3tools/contracts";
10import * as Data from "effect/Data";
10import * as Console from "effect/Console";
11import * as Context from "effect/Context";
1112import * as Crypto from "effect/Crypto";
13import * as DateTime from "effect/DateTime";
1214import * as Deferred from "effect/Deferred";
1315import * as Effect from "effect/Effect";
1416import * as Exit from "effect/Exit";
......@@ -17,23 +19,21 @@ import * as Option from "effect/Option";
1719import * as Path from "effect/Path";
1820import * as Queue from "effect/Queue";
1921import * as Ref from "effect/Ref";
22import * as Schema from "effect/Schema";
2023import * as Scope from "effect/Scope";
21import * as Context from "effect/Context";
22import * as Console from "effect/Console";
23import * as DateTime from "effect/DateTime";
2424
25import { ServerConfig } from "./config.ts";
26import { Keybindings } from "./keybindings.ts";
25import * as ServerConfig from "./config.ts";
26import * as Keybindings from "./keybindings.ts";
2727import * as ExternalLauncher from "./process/externalLauncher.ts";
28import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine.ts";
29import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts";
30import { OrchestrationReactor } from "./orchestration/Services/OrchestrationReactor.ts";
31import { ServerLifecycleEvents } from "./serverLifecycleEvents.ts";
32import { ServerSettingsService } from "./serverSettings.ts";
33import { ServerEnvironment } from "./environment/Services/ServerEnvironment.ts";
34import { AnalyticsService } from "./telemetry/Services/AnalyticsService.ts";
28import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts";
29import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts";
30import * as OrchestrationReactor from "./orchestration/Services/OrchestrationReactor.ts";
31import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";
32import * as ServerSettings from "./serverSettings.ts";
33import * as ServerEnvironment from "./environment/Services/ServerEnvironment.ts";
34import * as AnalyticsService from "./telemetry/Services/AnalyticsService.ts";
3535import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
36import { ProviderSessionReaper } from "./provider/Services/ProviderSessionReaper.ts";
36import * as ProviderSessionReaper from "./provider/Services/ProviderSessionReaper.ts";
3737import {
3838 formatHeadlessServeOutput,
3939 formatHostForUrl,
......@@ -41,22 +41,30 @@ import {
4141 issueHeadlessServeAccessInfo,
4242} from "./startupAccess.ts";
4343
44export class ServerRuntimeStartupError extends Data.TaggedError("ServerRuntimeStartupError")<{
45 readonly message: string;
46 readonly cause?: unknown;
47}> {}
48
49export interface ServerRuntimeStartupShape {
50 readonly awaitCommandReady: Effect.Effect<void, ServerRuntimeStartupError>;
51 readonly markHttpListening: Effect.Effect<void>;
52 readonly enqueueCommand: <A, E>(
53 effect: Effect.Effect<A, E>,
54 ) => Effect.Effect<A, E | ServerRuntimeStartupError>;
44export class ServerRuntimeStartupError extends Schema.TaggedErrorClass<ServerRuntimeStartupError>()(
45 "ServerRuntimeStartupError",
46 {
47 stage: Schema.Literal("command-readiness"),
48 cause: Schema.optional(Schema.Defect()),
49 },
50) {
51 override get message(): string {
52 switch (this.stage) {
53 case "command-readiness":
54 return "Server runtime startup failed before command readiness.";
55 }
56 }
5557}
5658
5759export class ServerRuntimeStartup extends Context.Service<
5860 ServerRuntimeStartup,
59 ServerRuntimeStartupShape
61 {
62 readonly awaitCommandReady: Effect.Effect<void, ServerRuntimeStartupError>;
63 readonly markHttpListening: Effect.Effect<void>;
64 readonly enqueueCommand: <A, E>(
65 effect: Effect.Effect<A, E>,
66 ) => Effect.Effect<A, E | ServerRuntimeStartupError>;
67 }
6068>()("t3/serverRuntimeStartup") {}
6169
6270interface QueuedCommand {
......@@ -124,8 +132,8 @@ export const makeCommandGate = Effect.gen(function* () {
124132});
125133
126134export const recordStartupHeartbeat = Effect.gen(function* () {
127 const analytics = yield* AnalyticsService;
128 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
135 const analytics = yield* AnalyticsService.AnalyticsService;
136 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
129137
130138 const { threadCount, projectCount } = yield* projectionSnapshotQuery.getCounts().pipe(
131139 Effect.catch((cause) =>
......@@ -160,7 +168,7 @@ export const getAutoBootstrapDefaultModelSelection = (): ModelSelection => ({
160168});
161169
162170export const resolveWelcomeBase = Effect.gen(function* () {
163 const serverConfig = yield* ServerConfig;
171 const serverConfig = yield* ServerConfig.ServerConfig;
164172 const segments = serverConfig.cwd.split(/[/\\]/).filter(Boolean);
165173 const projectName = segments[segments.length - 1] ?? "project";
166174
......@@ -173,9 +181,9 @@ export const resolveWelcomeBase = Effect.gen(function* () {
173181export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () {
174182 const crypto = yield* Crypto.Crypto;
175183 const randomUUID = crypto.randomUUIDv4;
176 const serverConfig = yield* ServerConfig;
177 const projectionReadModelQuery = yield* ProjectionSnapshotQuery;
178 const orchestrationEngine = yield* OrchestrationEngineService;
184 const serverConfig = yield* ServerConfig.ServerConfig;
185 const projectionReadModelQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
186 const orchestrationEngine = yield* OrchestrationEngine.OrchestrationEngineService;
179187 const path = yield* Path.Path;
180188
181189 let bootstrapProjectId: ProjectId | undefined;
......@@ -243,7 +251,7 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () {
243251});
244252
245253const resolveStartupBrowserTarget = Effect.gen(function* () {
246 const serverConfig = yield* ServerConfig;
254 const serverConfig = yield* ServerConfig.ServerConfig;
247255 const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;
248256 const localUrl = `http://localhost:${serverConfig.port}`;
249257 const bindUrl =
......@@ -260,7 +268,7 @@ const resolveStartupBrowserTarget = Effect.gen(function* () {
260268
261269const maybeOpenBrowser = (target: string) =>
262270 Effect.gen(function* () {
263 const serverConfig = yield* ServerConfig;
271 const serverConfig = yield* ServerConfig.ServerConfig;
264272 if (serverConfig.noBrowser) {
265273 return;
266274 }
......@@ -281,14 +289,14 @@ const runStartupPhase = <A, E, R>(phase: string, effect: Effect.Effect<A, E, R>)
281289 Effect.withSpan(`server.startup.${phase}`),
282290 );
283291
284export const makeServerRuntimeStartup = Effect.gen(function* () {
285 const serverConfig = yield* ServerConfig;
286 const keybindings = yield* Keybindings;
287 const orchestrationReactor = yield* OrchestrationReactor;
288 const providerSessionReaper = yield* ProviderSessionReaper;
289 const lifecycleEvents = yield* ServerLifecycleEvents;
290 const serverSettings = yield* ServerSettingsService;
291 const serverEnvironment = yield* ServerEnvironment;
292const make = Effect.gen(function* () {
293 const serverConfig = yield* ServerConfig.ServerConfig;
294 const keybindings = yield* Keybindings.Keybindings;
295 const orchestrationReactor = yield* OrchestrationReactor.OrchestrationReactor;
296 const providerSessionReaper = yield* ProviderSessionReaper.ProviderSessionReaper;
297 const lifecycleEvents = yield* ServerLifecycleEvents.ServerLifecycleEvents;
298 const serverSettings = yield* ServerSettings.ServerSettingsService;
299 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;
292300 const crypto = yield* Crypto.Crypto;
293301
294302 const commandGate = yield* makeCommandGate;
......@@ -409,7 +417,7 @@ export const makeServerRuntimeStartup = Effect.gen(function* () {
409417 const startupExit = yield* Effect.exit(startup);
410418 if (Exit.isFailure(startupExit)) {
411419 const error = new ServerRuntimeStartupError({
412 message: "Server runtime startup failed before command readiness.",
420 stage: "command-readiness",
413421 cause: startupExit.cause,
414422 });
415423 yield* Effect.logError("server runtime startup failed", { cause: startupExit.cause });
......@@ -461,10 +469,7 @@ export const makeServerRuntimeStartup = Effect.gen(function* () {
461469 awaitCommandReady: commandGate.awaitCommandReady,
462470 markHttpListening: Deferred.succeed(httpListening, undefined),
463471 enqueueCommand: commandGate.enqueueCommand,
464 } satisfies ServerRuntimeStartupShape;
472 } satisfies ServerRuntimeStartup["Service"];
465473});
466474
467export const ServerRuntimeStartupLive = Layer.effect(
468 ServerRuntimeStartup,
469 makeServerRuntimeStartup,
470);
475export const layer = Layer.effect(ServerRuntimeStartup, make);
apps/server/src/serverRuntimeState.ts+3-3
......@@ -5,7 +5,7 @@ import * as Option from "effect/Option";
55import * as Schema from "effect/Schema";
66
77import { writeFileStringAtomically } from "./atomicWrite.ts";
8import { type ServerConfigShape } from "./config.ts";
8import type * as ServerConfig from "./config.ts";
99import { formatHostForUrl, isWildcardHost } from "./startupAccess.ts";
1010
1111export const PersistedServerRuntimeState = Schema.Struct({
......@@ -23,7 +23,7 @@ const decodePersistedServerRuntimeState = Schema.decodeUnknownEffect(
2323);
2424
2525const runtimeOriginForConfig = (
26 config: Pick<ServerConfigShape, "host">,
26 config: Pick<ServerConfig.ServerConfig["Service"], "host">,
2727 port: number,
2828): PersistedServerRuntimeState["origin"] => {
2929 const hostname =
......@@ -32,7 +32,7 @@ const runtimeOriginForConfig = (
3232};
3333
3434export const makePersistedServerRuntimeState = (input: {
35 readonly config: Pick<ServerConfigShape, "host">;
35 readonly config: Pick<ServerConfig.ServerConfig["Service"], "host">;
3636 readonly port: number;
3737}): Effect.Effect<PersistedServerRuntimeState> =>
3838 Effect.map(DateTime.now, (now) => ({
apps/server/src/serverSettings.test.ts+19-17
......@@ -13,14 +13,16 @@ import * as Duration from "effect/Duration";
1313import * as FileSystem from "effect/FileSystem";
1414import * as Layer from "effect/Layer";
1515import * as Schema from "effect/Schema";
16import { ServerConfig } from "./config.ts";
17import { ServerSettingsLive, ServerSettingsService } from "./serverSettings.ts";
16import * as ServerSecretStore from "./auth/ServerSecretStore.ts";
17import * as ServerConfig from "./config.ts";
18import * as ServerSettingsModule from "./serverSettings.ts";
1819
1920const decodeSettingsPatch = Schema.decodeUnknownEffect(ServerSettingsPatch);
2021const decodeServerSettings = Schema.decodeUnknownEffect(ServerSettings);
2122
2223const makeServerSettingsLayer = () =>
23 ServerSettingsLive.pipe(
24 ServerSettingsModule.layer.pipe(
25 Layer.provide(ServerSecretStore.layer),
2426 Layer.provideMerge(
2527 Layer.fresh(
2628 ServerConfig.layerTest(process.cwd(), {
......@@ -77,7 +79,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
7779
7880 it.effect("deep merges nested settings updates without dropping siblings", () =>
7981 Effect.gen(function* () {
80 const serverSettings = yield* ServerSettingsService;
82 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
8183
8284 yield* serverSettings.updateSettings({
8385 providers: {
......@@ -145,7 +147,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
145147
146148 it.effect("preserves model when switching providers via textGenerationModelSelection", () =>
147149 Effect.gen(function* () {
148 const serverSettings = yield* ServerSettingsService;
150 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
149151
150152 // Start with Claude text generation selection
151153 yield* serverSettings.updateSettings({
......@@ -183,7 +185,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
183185
184186 it.effect("preserves custom provider instance text generation selections", () =>
185187 Effect.gen(function* () {
186 const serverSettings = yield* ServerSettingsService;
188 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
187189
188190 const next = yield* serverSettings.updateSettings({
189191 providerInstances: {
......@@ -210,7 +212,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
210212 "uses explicit provider instance enabled state over legacy provider enabled state",
211213 () =>
212214 Effect.gen(function* () {
213 const serverSettings = yield* ServerSettingsService;
215 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
214216 const instanceId = ProviderInstanceId.make("claude_openrouter");
215217
216218 const next = yield* serverSettings.updateSettings({
......@@ -241,7 +243,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
241243
242244 it.effect("preserves enabled text generation selections for non-built-in drivers", () =>
243245 Effect.gen(function* () {
244 const serverSettings = yield* ServerSettingsService;
246 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
245247 const instanceId = ProviderInstanceId.make("openrouter_text");
246248
247249 const next = yield* serverSettings.updateSettings({
......@@ -267,7 +269,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
267269
268270 it.effect("drops stale text generation options when resetting model selection", () =>
269271 Effect.gen(function* () {
270 const serverSettings = yield* ServerSettingsService;
272 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
271273
272274 yield* serverSettings.updateSettings({
273275 textGenerationModelSelection: {
......@@ -300,7 +302,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
300302
301303 it.effect("replaces provider instance maps when clearing optional fields", () =>
302304 Effect.gen(function* () {
303 const serverSettings = yield* ServerSettingsService;
305 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
304306 const codexId = ProviderInstanceId.make("codex");
305307
306308 yield* serverSettings.updateSettings({
......@@ -337,7 +339,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
337339
338340 it.effect("trims provider path settings when updates are applied", () =>
339341 Effect.gen(function* () {
340 const serverSettings = yield* ServerSettingsService;
342 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
341343
342344 const next = yield* serverSettings.updateSettings({
343345 providers: {
......@@ -382,7 +384,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
382384
383385 it.effect("trims observability settings when updates are applied", () =>
384386 Effect.gen(function* () {
385 const serverSettings = yield* ServerSettingsService;
387 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
386388
387389 const next = yield* serverSettings.updateSettings({
388390 addProjectBaseDirectory: " ~/Development ",
......@@ -402,7 +404,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
402404
403405 it.effect("defaults blank binary paths to provider executables", () =>
404406 Effect.gen(function* () {
405 const serverSettings = yield* ServerSettingsService;
407 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
406408
407409 const next = yield* serverSettings.updateSettings({
408410 providers: {
......@@ -422,8 +424,8 @@ it.layer(NodeServices.layer)("server settings", (it) => {
422424
423425 it.effect("writes only non-default server settings to disk", () =>
424426 Effect.gen(function* () {
425 const serverSettings = yield* ServerSettingsService;
426 const serverConfig = yield* ServerConfig;
427 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
428 const serverConfig = yield* ServerConfig.ServerConfig;
427429 const fileSystem = yield* FileSystem.FileSystem;
428430 const next = yield* serverSettings.updateSettings({
429431 addProjectBaseDirectory: "~/Development",
......@@ -469,8 +471,8 @@ it.layer(NodeServices.layer)("server settings", (it) => {
469471
470472 it.effect("stores sensitive provider instance environment values outside settings.json", () =>
471473 Effect.gen(function* () {
472 const serverSettings = yield* ServerSettingsService;
473 const serverConfig = yield* ServerConfig;
474 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
475 const serverConfig = yield* ServerConfig.ServerConfig;
474476 const fileSystem = yield* FileSystem.FileSystem;
475477 const instanceId = ProviderInstanceId.make("codex_personal");
476478
apps/server/src/serverSettings.ts+56-57
......@@ -26,26 +26,26 @@ import {
2626 type ServerSettingsPatch,
2727} from "@t3tools/contracts";
2828import * as Cache from "effect/Cache";
29import * as Cause from "effect/Cause";
30import * as Context from "effect/Context";
2931import * as Deferred from "effect/Deferred";
3032import * as Duration from "effect/Duration";
33import * as Equal from "effect/Equal";
3134import * as Effect from "effect/Effect";
3235import * as Exit from "effect/Exit";
3336import * as FileSystem from "effect/FileSystem";
3437import * as Layer from "effect/Layer";
3538import * as Option from "effect/Option";
3639import * as Path from "effect/Path";
37import * as Equal from "effect/Equal";
3840import * as PubSub from "effect/PubSub";
3941import * as Ref from "effect/Ref";
4042import * as Schema from "effect/Schema";
4143import * as SchemaIssue from "effect/SchemaIssue";
44import * as Semaphore from "effect/Semaphore";
4245import * as Scope from "effect/Scope";
43import * as Context from "effect/Context";
4446import * as Stream from "effect/Stream";
45import * as Cause from "effect/Cause";
46import * as Semaphore from "effect/Semaphore";
4747import { writeFileStringAtomically } from "./atomicWrite.ts";
48import { ServerConfig } from "./config.ts";
48import * as ServerConfig from "./config.ts";
4949import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct";
5050import { fromJsonStringPretty, fromLenientJson } from "@t3tools/shared/schemaJson";
5151import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
......@@ -109,59 +109,60 @@ export function redactServerSettingsForClient(settings: ServerSettings): ServerS
109109 return { ...settings, providerInstances };
110110}
111111
112export interface ServerSettingsShape {
113 /** Start the settings runtime and attach file watching. */
114 readonly start: Effect.Effect<void, ServerSettingsError>;
115
116 /** Await settings runtime readiness. */
117 readonly ready: Effect.Effect<void, ServerSettingsError>;
112export class ServerSettingsService extends Context.Service<
113 ServerSettingsService,
114 {
115 /** Start the settings runtime and attach file watching. */
116 readonly start: Effect.Effect<void, ServerSettingsError>;
118117
119 /** Read the current settings. */
120 readonly getSettings: Effect.Effect<ServerSettings, ServerSettingsError>;
118 /** Await settings runtime readiness. */
119 readonly ready: Effect.Effect<void, ServerSettingsError>;
121120
122 /** Patch settings and persist. Returns the new full settings object. */
123 readonly updateSettings: (
124 patch: ServerSettingsPatch,
125 ) => Effect.Effect<ServerSettings, ServerSettingsError>;
121 /** Read the current settings. */
122 readonly getSettings: Effect.Effect<ServerSettings, ServerSettingsError>;
126123
127 /** Stream of settings change events. */
128 readonly streamChanges: Stream.Stream<ServerSettings>;
129}
124 /** Patch settings and persist. Returns the new full settings object. */
125 readonly updateSettings: (
126 patch: ServerSettingsPatch,
127 ) => Effect.Effect<ServerSettings, ServerSettingsError>;
130128
131export class ServerSettingsService extends Context.Service<
132 ServerSettingsService,
133 ServerSettingsShape
129 /** Stream of settings change events. */
130 readonly streamChanges: Stream.Stream<ServerSettings>;
131 }
134132>()("t3/serverSettings/ServerSettingsService") {
135 static readonly layerTest = (overrides: DeepPartial<ServerSettings> = {}) =>
136 Layer.effect(
137 ServerSettingsService,
138 Effect.gen(function* () {
139 const { automaticGitFetchInterval, ...overridesForMerge } = overrides;
140 const merged = deepMerge(DEFAULT_SERVER_SETTINGS, overridesForMerge);
141 const initialSettings = yield* normalizeServerSettings({
142 ...merged,
143 ...(automaticGitFetchInterval !== undefined
144 ? { automaticGitFetchInterval: automaticGitFetchInterval as Duration.Duration }
145 : {}),
146 });
147 const currentSettingsRef = yield* Ref.make<ServerSettings>(initialSettings);
148
149 return {
150 start: Effect.void,
151 ready: Effect.void,
152 getSettings: Ref.get(currentSettingsRef),
153 updateSettings: (patch) =>
154 Ref.get(currentSettingsRef).pipe(
155 Effect.map((currentSettings) => applyServerSettingsPatch(currentSettings, patch)),
156 Effect.flatMap(normalizeServerSettings),
157 Effect.tap((nextSettings) => Ref.set(currentSettingsRef, nextSettings)),
158 ),
159 streamChanges: Stream.empty,
160 } satisfies ServerSettingsShape;
161 }),
162 );
133 /** @deprecated Import and use `layerTest` from this module. */
134 static readonly layerTest = (overrides: DeepPartial<ServerSettings> = {}) => layerTest(overrides);
163135}
164136
137const makeTest = (overrides: DeepPartial<ServerSettings> = {}) =>
138 Effect.gen(function* () {
139 const { automaticGitFetchInterval, ...overridesForMerge } = overrides;
140 const merged = deepMerge(DEFAULT_SERVER_SETTINGS, overridesForMerge);
141 const initialSettings = yield* normalizeServerSettings({
142 ...merged,
143 ...(automaticGitFetchInterval !== undefined
144 ? { automaticGitFetchInterval: automaticGitFetchInterval as Duration.Duration }
145 : {}),
146 });
147 const currentSettingsRef = yield* Ref.make<ServerSettings>(initialSettings);
148
149 return {
150 start: Effect.void,
151 ready: Effect.void,
152 getSettings: Ref.get(currentSettingsRef),
153 updateSettings: (patch) =>
154 Ref.get(currentSettingsRef).pipe(
155 Effect.map((currentSettings) => applyServerSettingsPatch(currentSettings, patch)),
156 Effect.flatMap(normalizeServerSettings),
157 Effect.tap((nextSettings) => Ref.set(currentSettingsRef, nextSettings)),
158 ),
159 streamChanges: Stream.empty,
160 } satisfies ServerSettingsService["Service"];
161 });
162
163export const layerTest = (overrides: DeepPartial<ServerSettings> = {}) =>
164 Layer.effect(ServerSettingsService, makeTest(overrides));
165
165166const ServerSettingsJson = fromLenientJson(ServerSettings);
166167const decodeServerSettingsJsonExit = Schema.decodeUnknownExit(ServerSettingsJson);
167168
......@@ -255,8 +256,8 @@ function stripDefaultServerSettings(current: unknown, defaults: unknown): unknow
255256 return Object.is(current, defaults) ? undefined : current;
256257}
257258
258const makeServerSettings = Effect.gen(function* () {
259 const { settingsPath } = yield* ServerConfig;
259const make = Effect.gen(function* () {
260 const { settingsPath } = yield* ServerConfig.ServerConfig;
260261 const fs = yield* FileSystem.FileSystem;
261262 const pathService = yield* Path.Path;
262263 const secretStore = yield* ServerSecretStore.ServerSecretStore;
......@@ -578,9 +579,7 @@ const makeServerSettings = Effect.gen(function* () {
578579 Stream.map(resolveTextGenerationProvider),
579580 );
580581 },
581 } satisfies ServerSettingsShape;
582 } satisfies ServerSettingsService["Service"];
582583});
583584
584export const ServerSettingsLive = Layer.effect(ServerSettingsService, makeServerSettings).pipe(
585 Layer.provide(ServerSecretStore.layer),
586);
585export const layer = Layer.effect(ServerSettingsService, make);