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";...@@ -4,27 +4,26 @@ import { expect, it } from "@effect/vitest";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
66
7import type { ServerConfigShape } from "../config.ts";7import * as ServerConfig from "../config.ts";
8import { ServerConfig } from "../config.ts";
9import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";8import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";
10import * as PairingGrantStore from "./PairingGrantStore.ts";9import * as PairingGrantStore from "./PairingGrantStore.ts";
11import * as EnvironmentAuth from "./EnvironmentAuth.ts";10import * as EnvironmentAuth from "./EnvironmentAuth.ts";
1211
13import * as ServerSecretStore from "./ServerSecretStore.ts";12import * as ServerSecretStore from "./ServerSecretStore.ts";
1413
15const makeServerConfigLayer = (overrides?: Partial<ServerConfigShape>) =>14const makeServerConfigLayer = (overrides?: Partial<ServerConfig.ServerConfig["Service"]>) =>
16 Layer.effect(15 Layer.effect(
17 ServerConfig,16 ServerConfig.ServerConfig,
18 Effect.gen(function* () {17 Effect.gen(function* () {
19 const config = yield* ServerConfig;18 const config = yield* ServerConfig.ServerConfig;
20 return {19 return {
21 ...config,20 ...config,
22 ...overrides,21 ...overrides,
23 } satisfies ServerConfigShape;22 } satisfies ServerConfig.ServerConfig["Service"];
24 }),23 }),
25 ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-server-test-" })));24 ).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"]>) =>
28 EnvironmentAuth.layer.pipe(27 EnvironmentAuth.layer.pipe(
29 Layer.provide(SqlitePersistenceMemory),28 Layer.provide(SqlitePersistenceMemory),
30 Layer.provide(ServerSecretStore.layer),29 Layer.provide(ServerSecretStore.layer),
...@@ -33,13 +32,15 @@ const makeEnvironmentAuthLayer = (overrides?: Partial<ServerConfigShape>) =>...@@ -33,13 +32,15 @@ const makeEnvironmentAuthLayer = (overrides?: Partial<ServerConfigShape>) =>
3332
34const makeCookieRequest = (33const makeCookieRequest = (
35 sessionToken: string,34 sessionToken: string,
36): Parameters<EnvironmentAuth.EnvironmentAuthShape["authenticateHttpRequest"]>[0] =>35): Parameters<EnvironmentAuth.EnvironmentAuth["Service"]["authenticateHttpRequest"]>[0] =>
37 ({36 ({
38 cookies: {37 cookies: {
39 t3_session: sessionToken,38 t3_session: sessionToken,
40 },39 },
41 headers: {},40 headers: {},
42 }) as unknown as Parameters<EnvironmentAuth.EnvironmentAuthShape["authenticateHttpRequest"]>[0];41 }) as unknown as Parameters<
42 EnvironmentAuth.EnvironmentAuth["Service"]["authenticateHttpRequest"]
43 >[0];
4344
44const requestMetadata = {45const requestMetadata = {
45 deviceType: "desktop" as const,46 deviceType: "desktop" as const,
apps/server/src/auth/EnvironmentAuthAdmin.test.ts+6-7
...@@ -3,31 +3,30 @@ import { expect, it } from "@effect/vitest";...@@ -3,31 +3,30 @@ import { expect, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
55
6import type { ServerConfigShape } from "../config.ts";6import * as ServerConfig from "../config.ts";
7import { ServerConfig } from "../config.ts";
8import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";7import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";
9import * as EnvironmentAuth from "./EnvironmentAuth.ts";8import * as EnvironmentAuth from "./EnvironmentAuth.ts";
10import * as ServerSecretStore from "./ServerSecretStore.ts";9import * as ServerSecretStore from "./ServerSecretStore.ts";
11import * as SessionStore from "./SessionStore.ts";10import * as SessionStore from "./SessionStore.ts";
1211
13const makeServerConfigLayer = (12const makeServerConfigLayer = (
14 overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,13 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
15) =>14) =>
16 Layer.effect(15 Layer.effect(
17 ServerConfig,16 ServerConfig.ServerConfig,
18 Effect.gen(function* () {17 Effect.gen(function* () {
19 const config = yield* ServerConfig;18 const config = yield* ServerConfig.ServerConfig;
20 return {19 return {
21 ...config,20 ...config,
22 ...overrides,21 ...overrides,
23 } satisfies ServerConfigShape;22 } satisfies ServerConfig.ServerConfig["Service"];
24 }),23 }),
25 ).pipe(24 ).pipe(
26 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-control-plane-test-" })),25 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-control-plane-test-" })),
27 );26 );
2827
29const makeEnvironmentAuthLayer = (28const makeEnvironmentAuthLayer = (
30 overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,29 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
31) =>30) =>
32 EnvironmentAuth.layer.pipe(31 EnvironmentAuth.layer.pipe(
33 Layer.provideMerge(ServerSecretStore.layer),32 Layer.provideMerge(ServerSecretStore.layer),
apps/server/src/auth/EnvironmentAuthPolicy.test.ts+7-6
...@@ -3,21 +3,22 @@ import { expect, it } from "@effect/vitest";...@@ -3,21 +3,22 @@ import { expect, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
55
6import type { ServerConfigShape } from "../config.ts";6import * as ServerConfig from "../config.ts";
7import { ServerConfig } from "../config.ts";
8import * as EnvironmentAuthPolicy from "./EnvironmentAuthPolicy.ts";7import * as EnvironmentAuthPolicy from "./EnvironmentAuthPolicy.ts";
98
10const makeEnvironmentAuthPolicyLayer = (overrides?: Partial<ServerConfigShape>) =>9const makeEnvironmentAuthPolicyLayer = (
10 overrides?: Partial<ServerConfig.ServerConfig["Service"]>,
11) =>
11 EnvironmentAuthPolicy.layer.pipe(12 EnvironmentAuthPolicy.layer.pipe(
12 Layer.provide(13 Layer.provide(
13 Layer.effect(14 Layer.effect(
14 ServerConfig,15 ServerConfig.ServerConfig,
15 Effect.gen(function* () {16 Effect.gen(function* () {
16 const config = yield* ServerConfig;17 const config = yield* ServerConfig.ServerConfig;
17 return {18 return {
18 ...config,19 ...config,
19 ...overrides,20 ...overrides,
20 } satisfies ServerConfigShape;21 } satisfies ServerConfig.ServerConfig["Service"];
21 }),22 }),
22 ).pipe(23 ).pipe(
23 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-policy-test-" })),24 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";...@@ -5,29 +5,28 @@ import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
6import * as TestClock from "effect/testing/TestClock";6import * as TestClock from "effect/testing/TestClock";
77
8import type { ServerConfigShape } from "../config.ts";8import * as ServerConfig from "../config.ts";
9import { ServerConfig } from "../config.ts";
10import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";9import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";
11import * as PairingGrantStore from "./PairingGrantStore.ts";10import * as PairingGrantStore from "./PairingGrantStore.ts";
1211
13const makeServerConfigLayer = (12const makeServerConfigLayer = (
14 overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,13 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
15) =>14) =>
16 Layer.effect(15 Layer.effect(
17 ServerConfig,16 ServerConfig.ServerConfig,
18 Effect.gen(function* () {17 Effect.gen(function* () {
19 const config = yield* ServerConfig;18 const config = yield* ServerConfig.ServerConfig;
20 return {19 return {
21 ...config,20 ...config,
22 ...overrides,21 ...overrides,
23 } satisfies ServerConfigShape;22 } satisfies ServerConfig.ServerConfig["Service"];
24 }),23 }),
25 ).pipe(24 ).pipe(
26 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-bootstrap-test-" })),25 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-bootstrap-test-" })),
27 );26 );
2827
29const makePairingGrantStoreLayer = (28const makePairingGrantStoreLayer = (
30 overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,29 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
31) =>30) =>
32 PairingGrantStore.layer.pipe(31 PairingGrantStore.layer.pipe(
33 Layer.provide(SqlitePersistenceMemory),32 Layer.provide(SqlitePersistenceMemory),
apps/server/src/auth/SessionStore.test.ts+8-9
...@@ -5,30 +5,29 @@ import * as Effect from "effect/Effect";...@@ -5,30 +5,29 @@ import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
6import * as TestClock from "effect/testing/TestClock";6import * as TestClock from "effect/testing/TestClock";
77
8import type { ServerConfigShape } from "../config.ts";8import * as ServerConfig from "../config.ts";
9import { ServerConfig } from "../config.ts";
10import { PersistenceSqlError } from "../persistence/Errors.ts";9import { PersistenceSqlError } from "../persistence/Errors.ts";
11import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";10import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";
12import { AuthSessionRepository } from "../persistence/Services/AuthSessions.ts";11import * as AuthSessions from "../persistence/Services/AuthSessions.ts";
13import * as SessionStore from "./SessionStore.ts";12import * as SessionStore from "./SessionStore.ts";
14import * as ServerSecretStore from "./ServerSecretStore.ts";13import * as ServerSecretStore from "./ServerSecretStore.ts";
1514
16const makeServerConfigLayer = (15const makeServerConfigLayer = (
17 overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,16 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
18) =>17) =>
19 Layer.effect(18 Layer.effect(
20 ServerConfig,19 ServerConfig.ServerConfig,
21 Effect.gen(function* () {20 Effect.gen(function* () {
22 const config = yield* ServerConfig;21 const config = yield* ServerConfig.ServerConfig;
23 return {22 return {
24 ...config,23 ...config,
25 ...overrides,24 ...overrides,
26 } satisfies ServerConfigShape;25 } satisfies ServerConfig.ServerConfig["Service"];
27 }),26 }),
28 ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-session-test-" })));27 ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-session-test-" })));
2928
30const makeSessionStoreLayer = (29const makeSessionStoreLayer = (
31 overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,30 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
32) =>31) =>
33 SessionStore.layer.pipe(32 SessionStore.layer.pipe(
34 Layer.provide(SqlitePersistenceMemory),33 Layer.provide(SqlitePersistenceMemory),
...@@ -41,7 +40,7 @@ const repositoryFailure = new PersistenceSqlError({...@@ -41,7 +40,7 @@ const repositoryFailure = new PersistenceSqlError({
41 detail: "sqlite is unavailable",40 detail: "sqlite is unavailable",
42});41});
4342
44const failingSessionLookupRepositoryLayer = Layer.succeed(AuthSessionRepository, {43const failingSessionLookupRepositoryLayer = Layer.succeed(AuthSessions.AuthSessionRepository, {
45 create: () => Effect.void,44 create: () => Effect.void,
46 getById: () => Effect.fail(repositoryFailure),45 getById: () => Effect.fail(repositoryFailure),
47 listActive: () => Effect.succeed([]),46 listActive: () => Effect.succeed([]),
apps/server/src/bin.test.ts+11-14
...@@ -20,8 +20,8 @@ import * as TestConsole from "effect/testing/TestConsole";...@@ -20,8 +20,8 @@ import * as TestConsole from "effect/testing/TestConsole";
20import { Command } from "effect/unstable/cli";20import { Command } from "effect/unstable/cli";
2121
22import { cli, makeCli } from "./bin.ts";22import { cli, makeCli } from "./bin.ts";
23import { deriveServerPaths, ServerConfig, type ServerConfigShape } from "./config.ts";23import * as ServerConfig from "./config.ts";
24import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts";24import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts";
25import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts";25import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts";
26import { orchestrationHttpApiLayer } from "./orchestration/http.ts";26import { orchestrationHttpApiLayer } from "./orchestration/http.ts";
27import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";27import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";
...@@ -57,7 +57,7 @@ const captureStdout = <A, E, R>(effect: Effect.Effect<A, E, R>) =>...@@ -57,7 +57,7 @@ const captureStdout = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
5757
58const makeCliTestServerConfig = (baseDir: string) =>58const makeCliTestServerConfig = (baseDir: string) =>
59 Effect.gen(function* () {59 Effect.gen(function* () {
60 const derivedPaths = yield* deriveServerPaths(baseDir, undefined);60 const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, undefined);
61 return {61 return {
62 logLevel: "Info",62 logLevel: "Info",
63 traceMinLevel: "Info",63 traceMinLevel: "Info",
...@@ -84,26 +84,23 @@ const makeCliTestServerConfig = (baseDir: string) =>...@@ -84,26 +84,23 @@ const makeCliTestServerConfig = (baseDir: string) =>
84 logWebSocketEvents: false,84 logWebSocketEvents: false,
85 tailscaleServeEnabled: false,85 tailscaleServeEnabled: false,
86 tailscaleServePort: 443,86 tailscaleServePort: 443,
87 } satisfies ServerConfigShape;87 } satisfies ServerConfig.ServerConfig["Service"];
88 });88 });
8989
90const makeProjectPersistenceLayer = (config: ServerConfigShape) =>90const makeProjectPersistenceLayer = (config: ServerConfig.ServerConfig["Service"]) =>
91 Layer.mergeAll(91 Layer.mergeAll(
92 OrchestrationLayerLive.pipe(92 OrchestrationLayerLive.pipe(
93 Layer.provideMerge(RepositoryIdentityResolverLive),93 Layer.provideMerge(RepositoryIdentityResolverLive),
94 Layer.provideMerge(SqlitePersistenceLayerLive),94 Layer.provideMerge(SqlitePersistenceLayerLive),
95 ),95 ),
96 WorkspacePathsLive,96 WorkspacePathsLive,
97 ).pipe(97 ).pipe(Layer.provideMerge(NodeServices.layer), Layer.provide(ServerConfig.layer(config)));
98 Layer.provideMerge(NodeServices.layer),
99 Layer.provide(Layer.succeed(ServerConfig, config)),
100 );
10198
102const readPersistedSnapshot = (baseDir: string) =>99const readPersistedSnapshot = (baseDir: string) =>
103 Effect.gen(function* () {100 Effect.gen(function* () {
104 const config = yield* makeCliTestServerConfig(baseDir);101 const config = yield* makeCliTestServerConfig(baseDir);
105 return yield* Effect.gen(function* () {102 return yield* Effect.gen(function* () {
106 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;103 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
107 return yield* projectionSnapshotQuery.getSnapshot();104 return yield* projectionSnapshotQuery.getSnapshot();
108 }).pipe(Effect.provide(makeProjectPersistenceLayer(config)));105 }).pipe(Effect.provide(makeProjectPersistenceLayer(config)));
109 });106 });
...@@ -133,7 +130,7 @@ const withLiveProjectCliServer = <A, E, R>(baseDir: string, run: () => Effect.Ef...@@ -133,7 +130,7 @@ const withLiveProjectCliServer = <A, E, R>(baseDir: string, run: () => Effect.Ef
133 }),130 }),
134 ),131 ),
135 Layer.provideMerge(NodeServices.layer),132 Layer.provideMerge(NodeServices.layer),
136 Layer.provide(Layer.succeed(ServerConfig, config)),133 Layer.provide(ServerConfig.layer(config)),
137 );134 );
138135
139 return yield* Effect.scoped(136 return yield* Effect.scoped(
...@@ -238,7 +235,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {...@@ -238,7 +235,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {
238 it.effect("logs in to headless connect without enabling access", () =>235 it.effect("logs in to headless connect without enabling access", () =>
239 Effect.gen(function* () {236 Effect.gen(function* () {
240 const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-cloud-login-test-"));237 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);
242 mkdirSync(secretsDir, { recursive: true });239 mkdirSync(secretsDir, { recursive: true });
243 writeFileSync(240 writeFileSync(
244 join(secretsDir, "cloud-cli-oauth-token.bin"),241 join(secretsDir, "cloud-cli-oauth-token.bin"),
...@@ -282,7 +279,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {...@@ -282,7 +279,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {
282 it.effect("logs out of headless connect and removes the stored CLI authorization", () =>279 it.effect("logs out of headless connect and removes the stored CLI authorization", () =>
283 Effect.gen(function* () {280 Effect.gen(function* () {
284 const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-cloud-logout-test-"));281 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);
286 const tokenPath = join(secretsDir, "cloud-cli-oauth-token.bin");283 const tokenPath = join(secretsDir, "cloud-cli-oauth-token.bin");
287 mkdirSync(secretsDir, { recursive: true });284 mkdirSync(secretsDir, { recursive: true });
288 writeFileSync(tokenPath, "invalid persisted token");285 writeFileSync(tokenPath, "invalid persisted token");
...@@ -461,7 +458,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {...@@ -461,7 +458,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {
461 "--base-dir",458 "--base-dir",
462 baseDir,459 baseDir,
463 ]);460 ]);
464 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;461 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
465 const readModel = yield* projectionSnapshotQuery.getSnapshot();462 const readModel = yield* projectionSnapshotQuery.getSnapshot();
466 const addedProject = readModel.projects.find(463 const addedProject = readModel.projects.find(
467 (project) => project.workspaceRoot === workspaceRoot && project.deletedAt === null,464 (project) => project.workspaceRoot === workspaceRoot && project.deletedAt === null,
apps/server/src/cli/auth.ts+3-3
...@@ -18,7 +18,7 @@ import {...@@ -18,7 +18,7 @@ import {
18 formatPairingCredentialList,18 formatPairingCredentialList,
19 formatSessionList,19 formatSessionList,
20} from "../cliAuthFormat.ts";20} from "../cliAuthFormat.ts";
21import { ServerConfig } from "../config.ts";21import * as ServerConfig from "../config.ts";
22import {22import {
23 authLocationFlags,23 authLocationFlags,
24 type CliAuthLocationFlags,24 type CliAuthLocationFlags,
...@@ -28,7 +28,7 @@ import {...@@ -28,7 +28,7 @@ import {
2828
29const runWithEnvironmentAuth = <A, E>(29const runWithEnvironmentAuth = <A, E>(
30 flags: CliAuthLocationFlags,30 flags: CliAuthLocationFlags,
31 run: (environmentAuth: EnvironmentAuth.EnvironmentAuthShape) => Effect.Effect<A, E>,31 run: (environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"]) => Effect.Effect<A, E>,
32 options?: {32 options?: {
33 readonly quietLogs?: boolean;33 readonly quietLogs?: boolean;
34 },34 },
...@@ -43,7 +43,7 @@ const runWithEnvironmentAuth = <A, E>(...@@ -43,7 +43,7 @@ const runWithEnvironmentAuth = <A, E>(
43 }).pipe(43 }).pipe(
44 Effect.provide(44 Effect.provide(
45 Layer.mergeAll(EnvironmentAuth.runtimeLayer).pipe(45 Layer.mergeAll(EnvironmentAuth.runtimeLayer).pipe(
46 Layer.provide(Layer.succeed(ServerConfig, config)),46 Layer.provide(ServerConfig.layer(config)),
47 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),47 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),
48 ),48 ),
49 ),49 ),
apps/server/src/cli/config.ts+12-20
...@@ -14,18 +14,10 @@ import * as SchemaTransformation from "effect/SchemaTransformation";...@@ -14,18 +14,10 @@ import * as SchemaTransformation from "effect/SchemaTransformation";
14import { Argument, Flag } from "effect/unstable/cli";14import { Argument, Flag } from "effect/unstable/cli";
1515
16import { readBootstrapEnvelope } from "../bootstrap.ts";16import { readBootstrapEnvelope } from "../bootstrap.ts";
17import {17import * as ServerConfig from "../config.ts";
18 DEFAULT_PORT,
19 deriveServerPaths,
20 ensureServerDirectories,
21 resolveStaticDir,
22 RuntimeMode,
23 type ServerConfigShape,
24 type StartupPresentation,
25} from "../config.ts";
26import { expandHomePath, resolveBaseDir } from "../os-jank.ts";18import { 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(
29 Flag.withDescription("Runtime mode. `desktop` keeps loopback defaults unless overridden."),21 Flag.withDescription("Runtime mode. `desktop` keeps loopback defaults unless overridden."),
30 Flag.optional,22 Flag.optional,
31);23);
...@@ -104,7 +96,7 @@ const EnvServerConfig = Config.all({...@@ -104,7 +96,7 @@ const EnvServerConfig = Config.all({
104 Config.withDefault(10_000),96 Config.withDefault(10_000),
105 ),97 ),
106 otlpServiceName: Config.string("T3CODE_OTLP_SERVICE_NAME").pipe(Config.withDefault("t3-server")),98 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(
108 Config.option,100 Config.option,
109 Config.map(Option.getOrUndefined),101 Config.map(Option.getOrUndefined),
110 ),102 ),
...@@ -139,7 +131,7 @@ const EnvServerConfig = Config.all({...@@ -139,7 +131,7 @@ const EnvServerConfig = Config.all({
139});131});
140132
141export interface CliServerFlags {133export interface CliServerFlags {
142 readonly mode: Option.Option<RuntimeMode>;134 readonly mode: Option.Option<ServerConfig.RuntimeMode>;
143 readonly port: Option.Option<number>;135 readonly port: Option.Option<number>;
144 readonly host: Option.Option<string>;136 readonly host: Option.Option<string>;
145 readonly baseDir: Option.Option<string>;137 readonly baseDir: Option.Option<string>;
...@@ -208,7 +200,7 @@ export const resolveServerConfig = (...@@ -208,7 +200,7 @@ export const resolveServerConfig = (
208 flags: CliServerFlags,200 flags: CliServerFlags,
209 cliLogLevel: Option.Option<LogLevel.LogLevel>,201 cliLogLevel: Option.Option<LogLevel.LogLevel>,
210 options?: {202 options?: {
211 readonly startupPresentation?: StartupPresentation;203 readonly startupPresentation?: ServerConfig.StartupPresentation;
212 readonly forceAutoBootstrapProjectFromCwd?: boolean;204 readonly forceAutoBootstrapProjectFromCwd?: boolean;
213 },205 },
214) =>206) =>
...@@ -238,7 +230,7 @@ export const resolveServerConfig = (...@@ -238,7 +230,7 @@ export const resolveServerConfig = (
238 : Option.none();230 : Option.none();
239 const bootstrap = Option.getOrUndefined(bootstrapEnvelope);231 const bootstrap = Option.getOrUndefined(bootstrapEnvelope);
240232
241 const mode: RuntimeMode = Option.getOrElse(233 const mode: ServerConfig.RuntimeMode = Option.getOrElse(
242 resolveOptionPrecedence(234 resolveOptionPrecedence(
243 normalizedFlags.mode,235 normalizedFlags.mode,
244 Option.fromUndefinedOr(env.mode),236 Option.fromUndefinedOr(env.mode),
...@@ -257,9 +249,9 @@ export const resolveServerConfig = (...@@ -257,9 +249,9 @@ export const resolveServerConfig = (
257 onSome: (value) => Effect.succeed(value),249 onSome: (value) => Effect.succeed(value),
258 onNone: () => {250 onNone: () => {
259 if (mode === "desktop") {251 if (mode === "desktop") {
260 return Effect.succeed(DEFAULT_PORT);252 return Effect.succeed(ServerConfig.DEFAULT_PORT);
261 }253 }
262 return findAvailablePort(DEFAULT_PORT);254 return findAvailablePort(ServerConfig.DEFAULT_PORT);
263 },255 },
264 },256 },
265 );257 );
...@@ -279,8 +271,8 @@ export const resolveServerConfig = (...@@ -279,8 +271,8 @@ export const resolveServerConfig = (
279 const rawCwd = Option.getOrElse(normalizedFlags.cwd, () => process.cwd());271 const rawCwd = Option.getOrElse(normalizedFlags.cwd, () => process.cwd());
280 const cwd = path.resolve(yield* expandHomePath(rawCwd.trim()));272 const cwd = path.resolve(yield* expandHomePath(rawCwd.trim()));
281 yield* fs.makeDirectory(cwd, { recursive: true });273 yield* fs.makeDirectory(cwd, { recursive: true });
282 const derivedPaths = yield* deriveServerPaths(baseDir, devUrl);274 const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, devUrl);
283 yield* ensureServerDirectories(derivedPaths);275 yield* ServerConfig.ensureServerDirectories(derivedPaths);
284 const persistedObservabilitySettings = yield* loadPersistedObservabilitySettings(276 const persistedObservabilitySettings = yield* loadPersistedObservabilitySettings(
285 derivedPaths.settingsPath,277 derivedPaths.settingsPath,
286 );278 );
...@@ -330,7 +322,7 @@ export const resolveServerConfig = (...@@ -330,7 +322,7 @@ export const resolveServerConfig = (
330 ),322 ),
331 () => 443,323 () => 443,
332 );324 );
333 const staticDir = devUrl ? undefined : yield* resolveStaticDir();325 const staticDir = devUrl ? undefined : yield* ServerConfig.resolveStaticDir();
334 const host = Option.getOrElse(326 const host = Option.getOrElse(
335 resolveOptionPrecedence(327 resolveOptionPrecedence(
336 normalizedFlags.host,328 normalizedFlags.host,
...@@ -341,7 +333,7 @@ export const resolveServerConfig = (...@@ -341,7 +333,7 @@ export const resolveServerConfig = (
341 );333 );
342 const logLevel = Option.getOrElse(cliLogLevel, () => env.logLevel);334 const logLevel = Option.getOrElse(cliLogLevel, () => env.logLevel);
343335
344 const config: ServerConfigShape = {336 const config: ServerConfig.ServerConfig["Service"] = {
345 logLevel,337 logLevel,
346 traceMinLevel: env.traceMinLevel,338 traceMinLevel: env.traceMinLevel,
347 traceTimingEnabled: env.traceTimingEnabled,339 traceTimingEnabled: env.traceTimingEnabled,
apps/server/src/cli/connect.ts+9-9
...@@ -31,9 +31,9 @@ import * as CliTokenManager from "../cloud/CliTokenManager.ts";...@@ -31,9 +31,9 @@ import * as CliTokenManager from "../cloud/CliTokenManager.ts";
31import { CLOUD_LINKED_USER_ID, RELAY_URL_SECRET } from "../cloud/config.ts";31import { CLOUD_LINKED_USER_ID, RELAY_URL_SECRET } from "../cloud/config.ts";
32import { relayUrlConfig } from "../cloud/publicConfig.ts";32import { relayUrlConfig } from "../cloud/publicConfig.ts";
33import { headlessRelayClientTracingLayer } from "../cloud/relayTracing.ts";33import { headlessRelayClientTracingLayer } from "../cloud/relayTracing.ts";
34import { ServerConfig } from "../config.ts";34import * as ServerConfig from "../config.ts";
35import { ServerEnvironmentLive } from "../environment/Layers/ServerEnvironment.ts";35import { ServerEnvironmentLive } from "../environment/Layers/ServerEnvironment.ts";
36import { ServerEnvironment } from "../environment/Services/ServerEnvironment.ts";36import * as ServerEnvironment from "../environment/Services/ServerEnvironment.ts";
37import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts";37import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts";
38import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts";38import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts";
3939
...@@ -145,7 +145,7 @@ const reportRelayClientInstallProgress = (event: RelayClientInstallProgressEvent...@@ -145,7 +145,7 @@ const reportRelayClientInstallProgress = (event: RelayClientInstallProgressEvent
145145
146export const acquireRelayClientForLink = Effect.fn("cloud.cli.acquire_relay_client_for_link")(146export const acquireRelayClientForLink = Effect.fn("cloud.cli.acquire_relay_client_for_link")(
147 function* <ConfirmError, ConfirmContext>(147 function* <ConfirmError, ConfirmContext>(
148 relayClient: RelayClient.RelayClientShape,148 relayClient: RelayClient.RelayClient["Service"],
149 confirmInstall: (version: string) => Effect.Effect<boolean, ConfirmError, ConfirmContext>,149 confirmInstall: (version: string) => Effect.Effect<boolean, ConfirmError, ConfirmContext>,
150 reportProgress: (event: RelayClientInstallProgressEvent) => Effect.Effect<void>,150 reportProgress: (event: RelayClientInstallProgressEvent) => Effect.Effect<void>,
151 ) {151 ) {
...@@ -164,7 +164,7 @@ export const acquireRelayClientForLink = Effect.fn("cloud.cli.acquire_relay_clie...@@ -164,7 +164,7 @@ export const acquireRelayClientForLink = Effect.fn("cloud.cli.acquire_relay_clie
164);164);
165165
166const withCloudCliSessionToken = <A, E, R>(166const withCloudCliSessionToken = <A, E, R>(
167 environmentAuth: EnvironmentAuth.EnvironmentAuthShape,167 environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"],
168 run: (token: string) => Effect.Effect<A, E, R>,168 run: (token: string) => Effect.Effect<A, E, R>,
169) =>169) =>
170 Effect.acquireUseRelease(170 Effect.acquireUseRelease(
...@@ -183,7 +183,7 @@ type LiveCloudActionResult =...@@ -183,7 +183,7 @@ type LiveCloudActionResult =
183 | { readonly status: "failed"; readonly cause: unknown };183 | { readonly status: "failed"; readonly cause: unknown };
184184
185const runLiveCloudUnlink = Effect.fn("cloud.cli.run_live_unlink")(function* () {185const runLiveCloudUnlink = Effect.fn("cloud.cli.run_live_unlink")(function* () {
186 const config = yield* ServerConfig;186 const config = yield* ServerConfig.ServerConfig;
187 const runtimeState = yield* readPersistedServerRuntimeState(config.serverRuntimeStatePath);187 const runtimeState = yield* readPersistedServerRuntimeState(config.serverRuntimeStatePath);
188 if (Option.isNone(runtimeState)) {188 if (Option.isNone(runtimeState)) {
189 return { status: "not-running" } satisfies LiveCloudActionResult;189 return { status: "not-running" } satisfies LiveCloudActionResult;
...@@ -219,7 +219,7 @@ const unlinkRelayEnvironment = Effect.fn("cloud.cli.unlink_relay_environment")(f...@@ -219,7 +219,7 @@ const unlinkRelayEnvironment = Effect.fn("cloud.cli.unlink_relay_environment")(f
219 return { status: "not-authenticated" } satisfies RelayUnlinkResult;219 return { status: "not-authenticated" } satisfies RelayUnlinkResult;
220 }220 }
221221
222 const environment = yield* ServerEnvironment;222 const environment = yield* ServerEnvironment.ServerEnvironment;
223 const environmentId = yield* environment.getEnvironmentId;223 const environmentId = yield* environment.getEnvironmentId;
224 const relayUrl = yield* relayUrlConfig;224 const relayUrl = yield* relayUrlConfig;
225 const httpClient = yield* HttpClient.HttpClient;225 const httpClient = yield* HttpClient.HttpClient;
...@@ -285,8 +285,8 @@ const runCloudCommand = <A, E>(...@@ -285,8 +285,8 @@ const runCloudCommand = <A, E>(
285 | FileSystem.FileSystem285 | FileSystem.FileSystem
286 | HttpClient.HttpClient286 | HttpClient.HttpClient
287 | Prompt.Environment287 | Prompt.Environment
288 | ServerConfig288 | ServerConfig.ServerConfig
289 | ServerEnvironment289 | ServerEnvironment.ServerEnvironment
290 >,290 >,
291 options?: {291 options?: {
292 readonly quietLogs?: boolean;292 readonly quietLogs?: boolean;
...@@ -305,7 +305,7 @@ const runCloudCommand = <A, E>(...@@ -305,7 +305,7 @@ const runCloudCommand = <A, E>(
305 headlessRelayClientTracingLayer,305 headlessRelayClientTracingLayer,
306 ).pipe(306 ).pipe(
307 Layer.provideMerge(FetchHttpClient.layer),307 Layer.provideMerge(FetchHttpClient.layer),
308 Layer.provideMerge(Layer.succeed(ServerConfig, config)),308 Layer.provideMerge(ServerConfig.layer(config)),
309 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),309 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),
310 );310 );
311 return yield* run.pipe(Effect.provide(runtimeLayer));311 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";...@@ -26,19 +26,19 @@ import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient";
2626
27import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";27import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
2828
29import { ServerConfig, type ServerConfigShape } from "../config.ts";29import * as ServerConfig from "../config.ts";
30import { OrchestrationEngineService } from "../orchestration/Services/OrchestrationEngine.ts";30import * as OrchestrationEngine from "../orchestration/Services/OrchestrationEngine.ts";
31import { ProjectionSnapshotQuery } from "../orchestration/Services/ProjectionSnapshotQuery.ts";31import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts";
32import { OrchestrationLayerLive } from "../orchestration/runtimeLayer.ts";32import { OrchestrationLayerLive } from "../orchestration/runtimeLayer.ts";
33import { layerConfig as SqlitePersistenceLayerLive } from "../persistence/Layers/Sqlite.ts";33import { layerConfig as SqlitePersistenceLayerLive } from "../persistence/Layers/Sqlite.ts";
34import { RepositoryIdentityResolverLive } from "../project/Layers/RepositoryIdentityResolver.ts";34import { RepositoryIdentityResolverLive } from "../project/Layers/RepositoryIdentityResolver.ts";
35import { getAutoBootstrapDefaultModelSelection } from "../serverRuntimeStartup.ts";35import * as ServerRuntimeStartup from "../serverRuntimeStartup.ts";
36import {36import {
37 clearPersistedServerRuntimeState,37 clearPersistedServerRuntimeState,
38 readPersistedServerRuntimeState,38 readPersistedServerRuntimeState,
39} from "../serverRuntimeState.ts";39} from "../serverRuntimeState.ts";
40import { WorkspacePathsLive } from "../workspace/Layers/WorkspacePaths.ts";40import { WorkspacePathsLive } from "../workspace/Layers/WorkspacePaths.ts";
41import { WorkspacePaths } from "../workspace/Services/WorkspacePaths.ts";41import * as WorkspacePaths from "../workspace/Services/WorkspacePaths.ts";
42import { type CliAuthLocationFlags, projectLocationFlags, resolveCliAuthConfig } from "./config.ts";42import { type CliAuthLocationFlags, projectLocationFlags, resolveCliAuthConfig } from "./config.ts";
4343
44type ProjectMutationTarget = {44type ProjectMutationTarget = {
...@@ -78,7 +78,7 @@ const ProjectCliRuntimeLive = Layer.mergeAll(...@@ -78,7 +78,7 @@ const ProjectCliRuntimeLive = Layer.mergeAll(
78const PROJECT_CLI_LIVE_SERVER_TIMEOUT = Duration.seconds(1);78const PROJECT_CLI_LIVE_SERVER_TIMEOUT = Duration.seconds(1);
79const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError);79const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError);
80const withProjectCliSessionToken = <A, E, R>(80const withProjectCliSessionToken = <A, E, R>(
81 environmentAuth: EnvironmentAuth.EnvironmentAuthShape,81 environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"],
82 run: (token: string) => Effect.Effect<A, E, R>,82 run: (token: string) => Effect.Effect<A, E, R>,
83) =>83) =>
84 Effect.acquireUseRelease(84 Effect.acquireUseRelease(
...@@ -123,7 +123,7 @@ const makeLiveServerClient = (origin: string) =>...@@ -123,7 +123,7 @@ const makeLiveServerClient = (origin: string) =>
123const normalizeWorkspaceRootForProjectCommand = Effect.fn(123const normalizeWorkspaceRootForProjectCommand = Effect.fn(
124 "normalizeWorkspaceRootForProjectCommand",124 "normalizeWorkspaceRootForProjectCommand",
125)(function* (workspaceRoot: string) {125)(function* (workspaceRoot: string) {
126 const workspacePaths = yield* WorkspacePaths;126 const workspacePaths = yield* WorkspacePaths.WorkspacePaths;
127 return yield* workspacePaths.normalizeWorkspaceRoot(workspaceRoot);127 return yield* workspacePaths.normalizeWorkspaceRoot(workspaceRoot);
128});128});
129129
...@@ -211,12 +211,15 @@ const dispatchLiveOrchestrationCommand = (...@@ -211,12 +211,15 @@ const dispatchLiveOrchestrationCommand = (
211 }).pipe(withProjectCliLiveServerTimeout, Effect.catch(failLiveServerRequest));211 }).pipe(withProjectCliLiveServerTimeout, Effect.catch(failLiveServerRequest));
212212
213const getOfflineSnapshot = Effect.fn("getOfflineSnapshot")(function* () {213const getOfflineSnapshot = Effect.fn("getOfflineSnapshot")(function* () {
214 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;214 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
215 return yield* projectionSnapshotQuery.getSnapshot();215 return yield* projectionSnapshotQuery.getSnapshot();
216});216});
217217
218const tryResolveLiveProjectExecutionMode = Effect.fn("tryResolveLiveProjectExecutionMode")(218const tryResolveLiveProjectExecutionMode = Effect.fn("tryResolveLiveProjectExecutionMode")(
219 function* (environmentAuth: EnvironmentAuth.EnvironmentAuthShape, config: ServerConfigShape) {219 function* (
220 environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"],
221 config: ServerConfig.ServerConfig["Service"],
222 ) {
220 const runtimeState = yield* readPersistedServerRuntimeState(config.serverRuntimeStatePath);223 const runtimeState = yield* readPersistedServerRuntimeState(config.serverRuntimeStatePath);
221 if (Option.isNone(runtimeState)) {224 if (Option.isNone(runtimeState)) {
222 return Option.none<{ readonly origin: string }>();225 return Option.none<{ readonly origin: string }>();
...@@ -251,7 +254,11 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (...@@ -251,7 +254,11 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (
251 }) => Effect.Effect<254 }) => Effect.Effect<
252 string,255 string,
253 Error,256 Error,
254 Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path | WorkspacePaths257 | Crypto.Crypto
258 | FileSystem.FileSystem
259 | HttpClient.HttpClient
260 | Path.Path
261 | WorkspacePaths.WorkspacePaths
255 >,262 >,
256) {263) {
257 const logLevel = yield* GlobalFlag.LogLevel;264 const logLevel = yield* GlobalFlag.LogLevel;
...@@ -278,13 +285,13 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (...@@ -278,13 +285,13 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (
278 }285 }
279286
280 const offlineRuntimeLayer = ProjectCliRuntimeLive.pipe(287 const offlineRuntimeLayer = ProjectCliRuntimeLive.pipe(
281 Layer.provide(Layer.succeed(ServerConfig, config)),288 Layer.provide(ServerConfig.layer(config)),
282 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),289 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),
283 );290 );
284291
285 return yield* Effect.gen(function* () {292 return yield* Effect.gen(function* () {
286 const snapshot = yield* getOfflineSnapshot();293 const snapshot = yield* getOfflineSnapshot();
287 const orchestrationEngine = yield* OrchestrationEngineService;294 const orchestrationEngine = yield* OrchestrationEngine.OrchestrationEngineService;
288 const output = yield* run({295 const output = yield* run({
289 snapshot,296 snapshot,
290 dispatch: (command) => orchestrationEngine.dispatch(command),297 dispatch: (command) => orchestrationEngine.dispatch(command),
...@@ -296,7 +303,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (...@@ -296,7 +303,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (
296 Effect.provide(303 Effect.provide(
297 Layer.mergeAll(EnvironmentAuth.runtimeLayer, WorkspacePathsLive).pipe(304 Layer.mergeAll(EnvironmentAuth.runtimeLayer, WorkspacePathsLive).pipe(
298 Layer.provideMerge(FetchHttpClient.layer),305 Layer.provideMerge(FetchHttpClient.layer),
299 Layer.provide(Layer.succeed(ServerConfig, config)),306 Layer.provide(ServerConfig.layer(config)),
300 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),307 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),
301 ),308 ),
302 ),309 ),
...@@ -341,7 +348,7 @@ const projectAddCommand = Command.make("add", {...@@ -341,7 +348,7 @@ const projectAddCommand = Command.make("add", {
341 projectId,348 projectId,
342 title,349 title,
343 workspaceRoot,350 workspaceRoot,
344 defaultModelSelection: getAutoBootstrapDefaultModelSelection(),351 defaultModelSelection: ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(),
345 createdAt: DateTime.formatIso(yield* DateTime.now),352 createdAt: DateTime.formatIso(yield* DateTime.now),
346 });353 });
347 return `Added project ${projectId} (${title}) at ${workspaceRoot}.`;354 return `Added project ${projectId} (${title}) at ${workspaceRoot}.`;
apps/server/src/config.ts+83-76
...@@ -6,13 +6,13 @@...@@ -6,13 +6,13 @@
6 *6 *
7 * @module ServerConfig7 * @module ServerConfig
8 */8 */
9import * as Context from "effect/Context";
9import * as Effect from "effect/Effect";10import * as Effect from "effect/Effect";
10import * as FileSystem from "effect/FileSystem";11import * as FileSystem from "effect/FileSystem";
11import * as Layer from "effect/Layer";12import * as Layer from "effect/Layer";
12import * as LogLevel from "effect/LogLevel";13import * as LogLevel from "effect/LogLevel";
13import * as Path from "effect/Path";14import * as Path from "effect/Path";
14import * as Schema from "effect/Schema";15import * as Schema from "effect/Schema";
15import * as Context from "effect/Context";
1616
17export const DEFAULT_PORT = 3773;17export const DEFAULT_PORT = 3773;
1818
...@@ -46,38 +46,51 @@ export interface ServerDerivedPaths {...@@ -46,38 +46,51 @@ export interface ServerDerivedPaths {
46}46}
4747
48/**48/**
49 * ServerConfigShape - Process/runtime configuration required by the server.49 * ServerConfig - Service tag for server runtime configuration.
50 */50 */
51export interface ServerConfigShape extends ServerDerivedPaths {51export class ServerConfig extends Context.Service<
52 readonly logLevel: LogLevel.LogLevel;52 ServerConfig,
53 readonly traceMinLevel: LogLevel.LogLevel;53 ServerDerivedPaths & {
54 readonly traceTimingEnabled: boolean;54 readonly logLevel: LogLevel.LogLevel;
55 readonly traceBatchWindowMs: number;55 readonly traceMinLevel: LogLevel.LogLevel;
56 readonly traceMaxBytes: number;56 readonly traceTimingEnabled: boolean;
57 readonly traceMaxFiles: number;57 readonly traceBatchWindowMs: number;
58 readonly otlpTracesUrl: string | undefined;58 readonly traceMaxBytes: number;
59 readonly otlpMetricsUrl: string | undefined;59 readonly traceMaxFiles: number;
60 readonly otlpExportIntervalMs: number;60 readonly otlpTracesUrl: string | undefined;
61 readonly otlpServiceName: string;61 readonly otlpMetricsUrl: string | undefined;
62 readonly mode: RuntimeMode;62 readonly otlpExportIntervalMs: number;
63 readonly port: number;63 readonly otlpServiceName: string;
64 readonly host: string | undefined;64 readonly mode: RuntimeMode;
65 readonly cwd: string;65 readonly port: number;
66 readonly baseDir: string;66 readonly host: string | undefined;
67 readonly staticDir: string | undefined;67 readonly cwd: string;
68 readonly devUrl: URL | undefined;68 readonly baseDir: string;
69 readonly noBrowser: boolean;69 readonly staticDir: string | undefined;
70 readonly startupPresentation: StartupPresentation;70 readonly devUrl: URL | undefined;
71 readonly desktopBootstrapToken: string | undefined;71 readonly noBrowser: boolean;
72 readonly autoBootstrapProjectFromCwd: boolean;72 readonly startupPresentation: StartupPresentation;
73 readonly logWebSocketEvents: boolean;73 readonly desktopBootstrapToken: string | undefined;
74 readonly tailscaleServeEnabled: boolean;74 readonly autoBootstrapProjectFromCwd: boolean;
75 readonly tailscaleServePort: number;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);
76}85}
7786
87export const make = (config: ServerConfig["Service"]) => ServerConfig.of(config);
88
89export const layer = (config: ServerConfig["Service"]) => Layer.succeed(ServerConfig, make(config));
90
78export const deriveServerPaths = Effect.fn(function* (91export const deriveServerPaths = Effect.fn(function* (
79 baseDir: ServerConfigShape["baseDir"],92 baseDir: ServerConfig["Service"]["baseDir"],
80 devUrl: ServerConfigShape["devUrl"],93 devUrl: ServerConfig["Service"]["devUrl"],
81): Effect.fn.Return<ServerDerivedPaths, never, Path.Path> {94): Effect.fn.Return<ServerDerivedPaths, never, Path.Path> {
82 const { join } = yield* Path.Path;95 const { join } = yield* Path.Path;
83 const stateDir = join(baseDir, devUrl !== undefined ? "dev" : "userdata");96 const stateDir = join(baseDir, devUrl !== undefined ? "dev" : "userdata");
...@@ -129,56 +142,50 @@ export const ensureServerDirectories = Effect.fn(function* (derivedPaths: Server...@@ -129,56 +142,50 @@ export const ensureServerDirectories = Effect.fn(function* (derivedPaths: Server
129 );142 );
130});143});
131144
132/**145const makeTest = Effect.fn("ServerConfig.makeTest")(function* (
133 * ServerConfig - Service tag for server runtime configuration.146 cwd: string,
134 */147 baseDirOrPrefix: string | { readonly prefix: string },
135export class ServerConfig extends Context.Service<ServerConfig, ServerConfigShape>()(
136 "t3/config/ServerConfig",
137) {148) {
138 static readonly layerTest = (cwd: string, baseDirOrPrefix: string | { prefix: string }) =>149 const devUrl = undefined;
139 Layer.effect(150 const fs = yield* FileSystem.FileSystem;
140 ServerConfig,151 const baseDir =
141 Effect.gen(function* () {152 typeof baseDirOrPrefix === "string"
142 const devUrl = undefined;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;158 return ServerConfig.of({
145 const baseDir =159 logLevel: "Error",
146 typeof baseDirOrPrefix === "string"160 traceMinLevel: "Info",
147 ? baseDirOrPrefix161 traceTimingEnabled: true,
148 : yield* fs.makeTempDirectoryScoped({ prefix: baseDirOrPrefix.prefix });162 traceBatchWindowMs: 200,
149 const derivedPaths = yield* deriveServerPaths(baseDir, devUrl);163 traceMaxBytes: 10 * 1024 * 1024,
150 yield* ensureServerDirectories(derivedPaths);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 {187export const layerTest = (cwd: string, baseDirOrPrefix: string | { readonly prefix: string }) =>
153 logLevel: "Error",188 Layer.effect(ServerConfig, makeTest(cwd, baseDirOrPrefix));
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}
182189
183export const resolveStaticDir = Effect.fn(function* () {190export const resolveStaticDir = Effect.fn(function* () {
184 const { join, resolve } = yield* Path.Path;191 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";...@@ -8,15 +8,15 @@ import * as FileSystem from "effect/FileSystem";
8import * as Layer from "effect/Layer";8import * as Layer from "effect/Layer";
9import * as PlatformError from "effect/PlatformError";9import * as PlatformError from "effect/PlatformError";
1010
11import { deriveServerPaths, ServerConfig, type ServerConfigShape } from "../../config.ts";11import * as ServerConfig from "../../config.ts";
12import { ServerEnvironment } from "../Services/ServerEnvironment.ts";12import * as ServerEnvironment from "../Services/ServerEnvironment.ts";
13import { ServerEnvironmentLive } from "./ServerEnvironment.ts";13import { ServerEnvironmentLive } from "./ServerEnvironment.ts";
1414
15const makeServerEnvironmentLayer = (baseDir: string) =>15const makeServerEnvironmentLayer = (baseDir: string) =>
16 ServerEnvironmentLive.pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)));16 ServerEnvironmentLive.pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)));
1717
18const makeServerConfig = Effect.fn(function* (baseDir: string) {18const makeServerConfig = Effect.fn(function* (baseDir: string) {
19 const derivedPaths = yield* deriveServerPaths(baseDir, undefined);19 const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, undefined);
2020
21 return {21 return {
22 ...derivedPaths,22 ...derivedPaths,
...@@ -44,7 +44,7 @@ const makeServerConfig = Effect.fn(function* (baseDir: string) {...@@ -44,7 +44,7 @@ const makeServerConfig = Effect.fn(function* (baseDir: string) {
44 devUrl: undefined,44 devUrl: undefined,
45 noBrowser: false,45 noBrowser: false,
46 startupPresentation: "browser",46 startupPresentation: "browser",
47 } satisfies ServerConfigShape;47 } satisfies ServerConfig.ServerConfig["Service"];
48});48});
4949
50it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {50it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
...@@ -56,11 +56,11 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {...@@ -56,11 +56,11 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
56 });56 });
5757
58 const first = yield* Effect.gen(function* () {58 const first = yield* Effect.gen(function* () {
59 const serverEnvironment = yield* ServerEnvironment;59 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;
60 return yield* serverEnvironment.getDescriptor;60 return yield* serverEnvironment.getDescriptor;
61 }).pipe(Effect.provide(makeServerEnvironmentLayer(baseDir)));61 }).pipe(Effect.provide(makeServerEnvironmentLayer(baseDir)));
62 const second = yield* Effect.gen(function* () {62 const second = yield* Effect.gen(function* () {
63 const serverEnvironment = yield* ServerEnvironment;63 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;
64 return yield* serverEnvironment.getDescriptor;64 return yield* serverEnvironment.getDescriptor;
65 }).pipe(Effect.provide(makeServerEnvironmentLayer(baseDir)));65 }).pipe(Effect.provide(makeServerEnvironmentLayer(baseDir)));
6666
...@@ -109,14 +109,12 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {...@@ -109,14 +109,12 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
109 });109 });
110110
111 const exit = yield* Effect.gen(function* () {111 const exit = yield* Effect.gen(function* () {
112 const serverEnvironment = yield* ServerEnvironment;112 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;
113 return yield* serverEnvironment.getDescriptor;113 return yield* serverEnvironment.getDescriptor;
114 }).pipe(114 }).pipe(
115 Effect.provide(115 Effect.provide(
116 ServerEnvironmentLive.pipe(116 ServerEnvironmentLive.pipe(
117 Layer.provide(117 Layer.provide(Layer.merge(ServerConfig.layer(serverConfig), failingFileSystemLayer)),
118 Layer.merge(Layer.succeed(ServerConfig, serverConfig), failingFileSystemLayer),
119 ),
120 ),118 ),
121 ),119 ),
122 Effect.exit,120 Effect.exit,
apps/server/src/keybindings.test.ts+48-55
...@@ -9,28 +9,21 @@ import * as Layer from "effect/Layer";...@@ -9,28 +9,21 @@ import * as Layer from "effect/Layer";
9import * as Logger from "effect/Logger";9import * as Logger from "effect/Logger";
10import * as Path from "effect/Path";10import * as Path from "effect/Path";
11import * as Schema from "effect/Schema";11import * as Schema from "effect/Schema";
12import { ServerConfig } from "./config.ts";12import * as ServerConfig from "./config.ts";
1313import * as Keybindings from "./keybindings.ts";
14import {
15 DEFAULT_KEYBINDINGS,
16 Keybindings,
17 KeybindingsLive,
18 ResolvedKeybindingFromConfig,
19 compileResolvedKeybindingRule,
20 compileResolvedKeybindingsConfig,
21 parseKeybindingShortcut,
22} from "./keybindings.ts";
23import { KeybindingsConfigError } from "@t3tools/contracts";14import { KeybindingsConfigError } from "@t3tools/contracts";
2415
25const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig);16const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig);
26const encodeKeybindingsConfigJson = Schema.encodeEffect(KeybindingsConfigJson);17const encodeKeybindingsConfigJson = Schema.encodeEffect(KeybindingsConfigJson);
27const decodeKeybindingsConfigJson = Schema.decodeUnknownEffect(KeybindingsConfigJson);18const decodeKeybindingsConfigJson = Schema.decodeUnknownEffect(KeybindingsConfigJson);
28const encodeResolvedKeybindingFromConfig = Schema.encodeEffect(ResolvedKeybindingFromConfig);19const encodeResolvedKeybindingFromConfig = Schema.encodeEffect(
20 Keybindings.ResolvedKeybindingFromConfig,
21);
29const decodeResolvedKeybindingFromConfigExit = Schema.decodeUnknownExit(22const decodeResolvedKeybindingFromConfigExit = Schema.decodeUnknownExit(
30 ResolvedKeybindingFromConfig,23 Keybindings.ResolvedKeybindingFromConfig,
31);24);
32const makeKeybindingsLayer = () => {25const makeKeybindingsLayer = () => {
33 return KeybindingsLive.pipe(26 return Keybindings.layer.pipe(
34 Layer.provideMerge(27 Layer.provideMerge(
35 Layer.fresh(28 Layer.fresh(
36 ServerConfig.layerTest(process.cwd(), {29 ServerConfig.layerTest(process.cwd(), {
...@@ -66,7 +59,7 @@ const readKeybindingsConfig = (configPath: string) =>...@@ -66,7 +59,7 @@ const readKeybindingsConfig = (configPath: string) =>
66it.layer(NodeServices.layer)("keybindings", (it) => {59it.layer(NodeServices.layer)("keybindings", (it) => {
67 it.effect("parses shortcuts including plus key", () =>60 it.effect("parses shortcuts including plus key", () =>
68 Effect.sync(() => {61 Effect.sync(() => {
69 assert.deepEqual(parseKeybindingShortcut("mod+j"), {62 assert.deepEqual(Keybindings.parseKeybindingShortcut("mod+j"), {
70 key: "j",63 key: "j",
71 metaKey: false,64 metaKey: false,
72 ctrlKey: false,65 ctrlKey: false,
...@@ -74,7 +67,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -74,7 +67,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
74 altKey: false,67 altKey: false,
75 modKey: true,68 modKey: true,
76 });69 });
77 assert.deepEqual(parseKeybindingShortcut("mod++"), {70 assert.deepEqual(Keybindings.parseKeybindingShortcut("mod++"), {
78 key: "+",71 key: "+",
79 metaKey: false,72 metaKey: false,
80 ctrlKey: false,73 ctrlKey: false,
...@@ -87,7 +80,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -87,7 +80,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
8780
88 it.effect("compiles valid rule with parsed when AST", () =>81 it.effect("compiles valid rule with parsed when AST", () =>
89 Effect.sync(() => {82 Effect.sync(() => {
90 const compiled = compileResolvedKeybindingRule({83 const compiled = Keybindings.compileResolvedKeybindingRule({
91 key: "mod+d",84 key: "mod+d",
92 command: "terminal.split",85 command: "terminal.split",
93 when: "terminalOpen && !terminalFocus",86 when: "terminalOpen && !terminalFocus",
...@@ -137,14 +130,14 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -137,14 +130,14 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
137 it.effect("rejects invalid rules", () =>130 it.effect("rejects invalid rules", () =>
138 Effect.sync(() => {131 Effect.sync(() => {
139 assert.isNull(132 assert.isNull(
140 compileResolvedKeybindingRule({133 Keybindings.compileResolvedKeybindingRule({
141 key: "mod+shift+d+o",134 key: "mod+shift+d+o",
142 command: "terminal.new",135 command: "terminal.new",
143 }),136 }),
144 );137 );
145138
146 assert.isNull(139 assert.isNull(
147 compileResolvedKeybindingRule({140 Keybindings.compileResolvedKeybindingRule({
148 key: "mod+d",141 key: "mod+d",
149 command: "terminal.split",142 command: "terminal.split",
150 when: "terminalFocus && (",143 when: "terminalFocus && (",
...@@ -152,7 +145,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -152,7 +145,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
152 );145 );
153146
154 assert.isNull(147 assert.isNull(
155 compileResolvedKeybindingRule({148 Keybindings.compileResolvedKeybindingRule({
156 key: "mod+d",149 key: "mod+d",
157 command: "terminal.split",150 command: "terminal.split",
158 when: `${"!".repeat(300)}terminalFocus`,151 when: `${"!".repeat(300)}terminalFocus`,
...@@ -181,23 +174,23 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -181,23 +174,23 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
181 it.effect("bootstraps default keybindings when config file is missing", () =>174 it.effect("bootstraps default keybindings when config file is missing", () =>
182 Effect.gen(function* () {175 Effect.gen(function* () {
183 const fs = yield* FileSystem.FileSystem;176 const fs = yield* FileSystem.FileSystem;
184 const { keybindingsConfigPath } = yield* ServerConfig;177 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
185 assert.isFalse(yield* fs.exists(keybindingsConfigPath));178 assert.isFalse(yield* fs.exists(keybindingsConfigPath));
186179
187 yield* Effect.gen(function* () {180 yield* Effect.gen(function* () {
188 const keybindings = yield* Keybindings;181 const keybindings = yield* Keybindings.Keybindings;
189 yield* keybindings.syncDefaultKeybindingsOnStartup;182 yield* keybindings.syncDefaultKeybindingsOnStartup;
190 });183 });
191184
192 const persisted = yield* readKeybindingsConfig(keybindingsConfigPath);185 const persisted = yield* readKeybindingsConfig(keybindingsConfigPath);
193 assert.deepEqual(persisted, DEFAULT_KEYBINDINGS);186 assert.deepEqual(persisted, Keybindings.DEFAULT_KEYBINDINGS);
194 }).pipe(Effect.provide(makeKeybindingsLayer())),187 }).pipe(Effect.provide(makeKeybindingsLayer())),
195 );188 );
196189
197 it.effect("ships configurable thread navigation defaults", () =>190 it.effect("ships configurable thread navigation defaults", () =>
198 Effect.sync(() => {191 Effect.sync(() => {
199 const defaultsByCommand = new Map(192 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),
201 );194 );
202195
203 assert.equal(defaultsByCommand.get("thread.previous"), "mod+shift+[");196 assert.equal(defaultsByCommand.get("thread.previous"), "mod+shift+[");
...@@ -215,17 +208,17 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -215,17 +208,17 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
215 it.effect("uses defaults in runtime when config is malformed without overriding file", () =>208 it.effect("uses defaults in runtime when config is malformed without overriding file", () =>
216 Effect.gen(function* () {209 Effect.gen(function* () {
217 const fs = yield* FileSystem.FileSystem;210 const fs = yield* FileSystem.FileSystem;
218 const { keybindingsConfigPath } = yield* ServerConfig;211 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
219 yield* fs.writeFileString(keybindingsConfigPath, "{ not-json");212 yield* fs.writeFileString(keybindingsConfigPath, "{ not-json");
220213
221 const configState = yield* Effect.gen(function* () {214 const configState = yield* Effect.gen(function* () {
222 const keybindings = yield* Keybindings;215 const keybindings = yield* Keybindings.Keybindings;
223 return yield* keybindings.loadConfigState;216 return yield* keybindings.loadConfigState;
224 });217 });
225218
226 assert.deepEqual(219 assert.deepEqual(
227 configState.keybindings,220 configState.keybindings,
228 compileResolvedKeybindingsConfig(DEFAULT_KEYBINDINGS),221 Keybindings.compileResolvedKeybindingsConfig(Keybindings.DEFAULT_KEYBINDINGS),
229 );222 );
230 assert.deepEqual(configState.issues, [223 assert.deepEqual(configState.issues, [
231 {224 {
...@@ -240,7 +233,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -240,7 +233,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
240 it.effect("ignores invalid entries in runtime and reports them as issues", () =>233 it.effect("ignores invalid entries in runtime and reports them as issues", () =>
241 Effect.gen(function* () {234 Effect.gen(function* () {
242 const fs = yield* FileSystem.FileSystem;235 const fs = yield* FileSystem.FileSystem;
243 const { keybindingsConfigPath } = yield* ServerConfig;236 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
244 yield* fs.writeFileString(237 yield* fs.writeFileString(
245 keybindingsConfigPath,238 keybindingsConfigPath,
246 // @effect-diagnostics-next-line preferSchemaOverJson:off239 // @effect-diagnostics-next-line preferSchemaOverJson:off
...@@ -252,7 +245,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -252,7 +245,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
252 );245 );
253246
254 const configState = yield* Effect.gen(function* () {247 const configState = yield* Effect.gen(function* () {
255 const keybindings = yield* Keybindings;248 const keybindings = yield* Keybindings.Keybindings;
256 return yield* keybindings.loadConfigState;249 return yield* keybindings.loadConfigState;
257 });250 });
258251
...@@ -279,14 +272,14 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -279,14 +272,14 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
279 "upserts missing default keybindings on startup without overriding existing command rules",272 "upserts missing default keybindings on startup without overriding existing command rules",
280 () =>273 () =>
281 Effect.gen(function* () {274 Effect.gen(function* () {
282 const { keybindingsConfigPath } = yield* ServerConfig;275 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
283 yield* writeKeybindingsConfig(keybindingsConfigPath, [276 yield* writeKeybindingsConfig(keybindingsConfigPath, [
284 { key: "mod+shift+t", command: "terminal.toggle" },277 { key: "mod+shift+t", command: "terminal.toggle" },
285 { key: "mod+shift+r", command: "script.run-tests.run" },278 { key: "mod+shift+r", command: "script.run-tests.run" },
286 ]);279 ]);
287280
288 yield* Effect.gen(function* () {281 yield* Effect.gen(function* () {
289 const keybindings = yield* Keybindings;282 const keybindings = yield* Keybindings.Keybindings;
290 yield* keybindings.syncDefaultKeybindingsOnStartup;283 yield* keybindings.syncDefaultKeybindingsOnStartup;
291 });284 });
292285
...@@ -300,7 +293,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -300,7 +293,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
300 persisted.some((entry) => entry.command === "terminal.toggle" && entry.key === "mod+j"),293 persisted.some((entry) => entry.command === "terminal.toggle" && entry.key === "mod+j"),
301 );294 );
302295
303 for (const defaultRule of DEFAULT_KEYBINDINGS) {296 for (const defaultRule of Keybindings.DEFAULT_KEYBINDINGS) {
304 assert.isTrue(byCommand.has(defaultRule.command), `expected ${defaultRule.command}`);297 assert.isTrue(byCommand.has(defaultRule.command), `expected ${defaultRule.command}`);
305 }298 }
306 assert.isTrue(byCommand.has("script.run-tests.run"));299 assert.isTrue(byCommand.has("script.run-tests.run"));
...@@ -314,13 +307,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -314,13 +307,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
314 });307 });
315308
316 return Effect.gen(function* () {309 return Effect.gen(function* () {
317 const { keybindingsConfigPath } = yield* ServerConfig;310 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
318 yield* writeKeybindingsConfig(keybindingsConfigPath, [311 yield* writeKeybindingsConfig(keybindingsConfigPath, [
319 { key: "mod+j", command: "script.custom-action.run" },312 { key: "mod+j", command: "script.custom-action.run" },
320 ]);313 ]);
321314
322 yield* Effect.gen(function* () {315 yield* Effect.gen(function* () {
323 const keybindings = yield* Keybindings;316 const keybindings = yield* Keybindings.Keybindings;
324 yield* keybindings.syncDefaultKeybindingsOnStartup;317 yield* keybindings.syncDefaultKeybindingsOnStartup;
325 });318 });
326319
...@@ -345,13 +338,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -345,13 +338,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
345338
346 it.effect("upserts custom keybindings to configured path", () =>339 it.effect("upserts custom keybindings to configured path", () =>
347 Effect.gen(function* () {340 Effect.gen(function* () {
348 const { keybindingsConfigPath } = yield* ServerConfig;341 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
349 yield* writeKeybindingsConfig(keybindingsConfigPath, [342 yield* writeKeybindingsConfig(keybindingsConfigPath, [
350 { key: "mod+j", command: "terminal.toggle" },343 { key: "mod+j", command: "terminal.toggle" },
351 ]);344 ]);
352345
353 const resolved = yield* Effect.gen(function* () {346 const resolved = yield* Effect.gen(function* () {
354 const keybindings = yield* Keybindings;347 const keybindings = yield* Keybindings.Keybindings;
355 return yield* keybindings.upsertKeybindingRule({348 return yield* keybindings.upsertKeybindingRule({
356 key: "mod+shift+r",349 key: "mod+shift+r",
357 command: "script.run-tests.run",350 command: "script.run-tests.run",
...@@ -371,12 +364,12 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -371,12 +364,12 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
371364
372 it.effect("appends additional custom keybindings for the same command", () =>365 it.effect("appends additional custom keybindings for the same command", () =>
373 Effect.gen(function* () {366 Effect.gen(function* () {
374 const { keybindingsConfigPath } = yield* ServerConfig;367 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
375 yield* writeKeybindingsConfig(keybindingsConfigPath, [368 yield* writeKeybindingsConfig(keybindingsConfigPath, [
376 { key: "mod+r", command: "script.run-tests.run" },369 { key: "mod+r", command: "script.run-tests.run" },
377 ]);370 ]);
378 yield* Effect.gen(function* () {371 yield* Effect.gen(function* () {
379 const keybindings = yield* Keybindings;372 const keybindings = yield* Keybindings.Keybindings;
380 return yield* keybindings.upsertKeybindingRule({373 return yield* keybindings.upsertKeybindingRule({
381 key: "mod+shift+r",374 key: "mod+shift+r",
382 command: "script.run-tests.run",375 command: "script.run-tests.run",
...@@ -394,13 +387,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -394,13 +387,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
394387
395 it.effect("replaces only the targeted custom keybinding", () =>388 it.effect("replaces only the targeted custom keybinding", () =>
396 Effect.gen(function* () {389 Effect.gen(function* () {
397 const { keybindingsConfigPath } = yield* ServerConfig;390 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
398 yield* writeKeybindingsConfig(keybindingsConfigPath, [391 yield* writeKeybindingsConfig(keybindingsConfigPath, [
399 { key: "mod+r", command: "script.run-tests.run" },392 { key: "mod+r", command: "script.run-tests.run" },
400 { key: "mod+shift+r", command: "script.run-tests.run" },393 { key: "mod+shift+r", command: "script.run-tests.run" },
401 ]);394 ]);
402 yield* Effect.gen(function* () {395 yield* Effect.gen(function* () {
403 const keybindings = yield* Keybindings;396 const keybindings = yield* Keybindings.Keybindings;
404 return yield* keybindings.upsertKeybindingRule({397 return yield* keybindings.upsertKeybindingRule({
405 key: "mod+alt+r",398 key: "mod+alt+r",
406 command: "script.run-tests.run",399 command: "script.run-tests.run",
...@@ -419,13 +412,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -419,13 +412,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
419412
420 it.effect("removes only the targeted custom keybinding", () =>413 it.effect("removes only the targeted custom keybinding", () =>
421 Effect.gen(function* () {414 Effect.gen(function* () {
422 const { keybindingsConfigPath } = yield* ServerConfig;415 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
423 yield* writeKeybindingsConfig(keybindingsConfigPath, [416 yield* writeKeybindingsConfig(keybindingsConfigPath, [
424 { key: "mod+r", command: "script.run-tests.run" },417 { key: "mod+r", command: "script.run-tests.run" },
425 { key: "mod+shift+r", command: "script.run-tests.run" },418 { key: "mod+shift+r", command: "script.run-tests.run" },
426 ]);419 ]);
427 yield* Effect.gen(function* () {420 yield* Effect.gen(function* () {
428 const keybindings = yield* Keybindings;421 const keybindings = yield* Keybindings.Keybindings;
429 return yield* keybindings.removeKeybindingRule({422 return yield* keybindings.removeKeybindingRule({
430 key: "mod+r",423 key: "mod+r",
431 command: "script.run-tests.run",424 command: "script.run-tests.run",
...@@ -441,11 +434,11 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -441,11 +434,11 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
441 it.effect("refuses to overwrite malformed keybindings config", () =>434 it.effect("refuses to overwrite malformed keybindings config", () =>
442 Effect.gen(function* () {435 Effect.gen(function* () {
443 const fs = yield* FileSystem.FileSystem;436 const fs = yield* FileSystem.FileSystem;
444 const { keybindingsConfigPath } = yield* ServerConfig;437 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
445 yield* fs.writeFileString(keybindingsConfigPath, "{ not-json");438 yield* fs.writeFileString(keybindingsConfigPath, "{ not-json");
446439
447 const result = yield* Effect.gen(function* () {440 const result = yield* Effect.gen(function* () {
448 const keybindings = yield* Keybindings;441 const keybindings = yield* Keybindings.Keybindings;
449 return yield* keybindings.upsertKeybindingRule({442 return yield* keybindings.upsertKeybindingRule({
450 key: "mod+shift+r",443 key: "mod+shift+r",
451 command: "script.run-tests.run",444 command: "script.run-tests.run",
...@@ -461,14 +454,14 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -461,14 +454,14 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
461 it.effect("reports non-array config parse errors without duplicate prefix", () =>454 it.effect("reports non-array config parse errors without duplicate prefix", () =>
462 Effect.gen(function* () {455 Effect.gen(function* () {
463 const fs = yield* FileSystem.FileSystem;456 const fs = yield* FileSystem.FileSystem;
464 const { keybindingsConfigPath } = yield* ServerConfig;457 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
465 yield* fs.writeFileString(458 yield* fs.writeFileString(
466 keybindingsConfigPath,459 keybindingsConfigPath,
467 '{"key":"mod+j","command":"terminal.toggle"}',460 '{"key":"mod+j","command":"terminal.toggle"}',
468 );461 );
469462
470 const firstResult = yield* Effect.gen(function* () {463 const firstResult = yield* Effect.gen(function* () {
471 const keybindings = yield* Keybindings;464 const keybindings = yield* Keybindings.Keybindings;
472 return yield* keybindings.upsertKeybindingRule({465 return yield* keybindings.upsertKeybindingRule({
473 key: "mod+shift+r",466 key: "mod+shift+r",
474 command: "script.run-tests.run",467 command: "script.run-tests.run",
...@@ -477,7 +470,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -477,7 +470,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
477 assertFailure(firstResult, "expected JSON array");470 assertFailure(firstResult, "expected JSON array");
478471
479 const secondResult = yield* Effect.gen(function* () {472 const secondResult = yield* Effect.gen(function* () {
480 const keybindings = yield* Keybindings;473 const keybindings = yield* Keybindings.Keybindings;
481 return yield* keybindings.upsertKeybindingRule({474 return yield* keybindings.upsertKeybindingRule({
482 key: "mod+shift+r",475 key: "mod+shift+r",
483 command: "script.run-tests.run",476 command: "script.run-tests.run",
...@@ -490,7 +483,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -490,7 +483,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
490 it.effect("fails when config directory is not writable", () =>483 it.effect("fails when config directory is not writable", () =>
491 Effect.gen(function* () {484 Effect.gen(function* () {
492 const fs = yield* FileSystem.FileSystem;485 const fs = yield* FileSystem.FileSystem;
493 const { keybindingsConfigPath } = yield* ServerConfig;486 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
494 const { dirname } = yield* Path.Path;487 const { dirname } = yield* Path.Path;
495 yield* writeKeybindingsConfig(keybindingsConfigPath, [488 yield* writeKeybindingsConfig(keybindingsConfigPath, [
496 { key: "mod+j", command: "terminal.toggle" },489 { key: "mod+j", command: "terminal.toggle" },
...@@ -498,7 +491,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -498,7 +491,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
498 yield* fs.chmod(dirname(keybindingsConfigPath), 0o500);491 yield* fs.chmod(dirname(keybindingsConfigPath), 0o500);
499492
500 const result = yield* Effect.gen(function* () {493 const result = yield* Effect.gen(function* () {
501 const keybindings = yield* Keybindings;494 const keybindings = yield* Keybindings.Keybindings;
502 return yield* keybindings.upsertKeybindingRule({495 return yield* keybindings.upsertKeybindingRule({
503 key: "mod+shift+r",496 key: "mod+shift+r",
504 command: "script.run-tests.run",497 command: "script.run-tests.run",
...@@ -516,13 +509,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -516,13 +509,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
516509
517 it.effect("caches loaded resolved config across repeated reads", () =>510 it.effect("caches loaded resolved config across repeated reads", () =>
518 Effect.gen(function* () {511 Effect.gen(function* () {
519 const { keybindingsConfigPath } = yield* ServerConfig;512 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
520 yield* writeKeybindingsConfig(keybindingsConfigPath, [513 yield* writeKeybindingsConfig(keybindingsConfigPath, [
521 { key: "mod+j", command: "terminal.toggle" },514 { key: "mod+j", command: "terminal.toggle" },
522 ]);515 ]);
523516
524 const [first, second] = yield* Effect.gen(function* () {517 const [first, second] = yield* Effect.gen(function* () {
525 const keybindings = yield* Keybindings;518 const keybindings = yield* Keybindings.Keybindings;
526 const firstLoad = (yield* keybindings.loadConfigState).keybindings;519 const firstLoad = (yield* keybindings.loadConfigState).keybindings;
527 const secondLoad = (yield* keybindings.loadConfigState).keybindings;520 const secondLoad = (yield* keybindings.loadConfigState).keybindings;
528 return [firstLoad, secondLoad] as const;521 return [firstLoad, secondLoad] as const;
...@@ -535,13 +528,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -535,13 +528,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
535528
536 it.effect("updates cached resolved config after upsert", () =>529 it.effect("updates cached resolved config after upsert", () =>
537 Effect.gen(function* () {530 Effect.gen(function* () {
538 const { keybindingsConfigPath } = yield* ServerConfig;531 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
539 yield* writeKeybindingsConfig(keybindingsConfigPath, [532 yield* writeKeybindingsConfig(keybindingsConfigPath, [
540 { key: "mod+j", command: "terminal.toggle" },533 { key: "mod+j", command: "terminal.toggle" },
541 ]);534 ]);
542535
543 const loadedAfterUpsert = yield* Effect.gen(function* () {536 const loadedAfterUpsert = yield* Effect.gen(function* () {
544 const keybindings = yield* Keybindings;537 const keybindings = yield* Keybindings.Keybindings;
545 yield* keybindings.loadConfigState;538 yield* keybindings.loadConfigState;
546 yield* keybindings.upsertKeybindingRule({539 yield* keybindings.upsertKeybindingRule({
547 key: "mod+shift+r",540 key: "mod+shift+r",
...@@ -557,7 +550,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -557,7 +550,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
557550
558 it.effect("serializes concurrent upserts to avoid lost updates", () =>551 it.effect("serializes concurrent upserts to avoid lost updates", () =>
559 Effect.gen(function* () {552 Effect.gen(function* () {
560 const { keybindingsConfigPath } = yield* ServerConfig;553 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
561 yield* writeKeybindingsConfig(keybindingsConfigPath, []);554 yield* writeKeybindingsConfig(keybindingsConfigPath, []);
562555
563 const commands = Array.from(556 const commands = Array.from(
...@@ -565,7 +558,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -565,7 +558,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
565 (_, index): KeybindingCommand => `script.concurrent-${index}.run`,558 (_, index): KeybindingCommand => `script.concurrent-${index}.run`,
566 );559 );
567 yield* Effect.gen(function* () {560 yield* Effect.gen(function* () {
568 const keybindings = yield* Keybindings;561 const keybindings = yield* Keybindings.Keybindings;
569 yield* Effect.all(562 yield* Effect.all(
570 commands.map((command, index) =>563 commands.map((command, index) =>
571 keybindings.upsertKeybindingRule({564 keybindings.upsertKeybindingRule({
apps/server/src/keybindings.ts+63-67
...@@ -41,7 +41,7 @@ import * as Context from "effect/Context";...@@ -41,7 +41,7 @@ import * as Context from "effect/Context";
41import * as Scope from "effect/Scope";41import * as Scope from "effect/Scope";
42import * as Stream from "effect/Stream";42import * as Stream from "effect/Stream";
43import * as Semaphore from "effect/Semaphore";43import * as Semaphore from "effect/Semaphore";
44import { ServerConfig } from "./config.ts";44import * as ServerConfig from "./config.ts";
45import { writeFileStringAtomically } from "./atomicWrite.ts";45import { writeFileStringAtomically } from "./atomicWrite.ts";
46import { fromJsonStringPretty, fromLenientJson } from "@t3tools/shared/schemaJson";46import { fromJsonStringPretty, fromLenientJson } from "@t3tools/shared/schemaJson";
47import {47import {
...@@ -225,74 +225,70 @@ function mergeWithDefaultKeybindings(custom: ResolvedKeybindingsConfig): Resolve...@@ -225,74 +225,70 @@ function mergeWithDefaultKeybindings(custom: ResolvedKeybindingsConfig): Resolve
225 return merged.slice(-MAX_KEYBINDINGS_COUNT);225 return merged.slice(-MAX_KEYBINDINGS_COUNT);
226}226}
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
287/**228/**
288 * Keybindings - Service tag for keybinding configuration operations.229 * Keybindings - Service tag for keybinding configuration operations.
289 */230 */
290export class Keybindings extends Context.Service<Keybindings, KeybindingsShape>()(231export class Keybindings extends Context.Service<
291 "t3/keybindings",232 Keybindings,
292) {}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* () {290const make = Effect.gen(function* () {
295 const { keybindingsConfigPath } = yield* ServerConfig;291 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
296 const fs = yield* FileSystem.FileSystem;292 const fs = yield* FileSystem.FileSystem;
297 const path = yield* Path.Path;293 const path = yield* Path.Path;
298 const upsertSemaphore = yield* Semaphore.make(1);294 const upsertSemaphore = yield* Semaphore.make(1);
...@@ -700,7 +696,7 @@ const makeKeybindings = Effect.gen(function* () {...@@ -700,7 +696,7 @@ const makeKeybindings = Effect.gen(function* () {
700 return nextResolved;696 return nextResolved;
701 }),697 }),
702 ),698 ),
703 } satisfies KeybindingsShape;699 } satisfies Keybindings["Service"];
704});700});
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";...@@ -32,8 +32,8 @@ import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
3232
33import { checkCodexProviderStatus, type CodexAppServerProviderSnapshot } from "./CodexProvider.ts";33import { checkCodexProviderStatus, type CodexAppServerProviderSnapshot } from "./CodexProvider.ts";
34import { checkClaudeProviderStatus } from "./ClaudeProvider.ts";34import { checkClaudeProviderStatus } from "./ClaudeProvider.ts";
35import { OpenCodeRuntimeLive } from "../opencodeRuntime.ts";35import * as OpenCodeRuntime from "../opencodeRuntime.ts";
36import { NoOpProviderEventLoggers, ProviderEventLoggers } from "./ProviderEventLoggers.ts";36import * as ProviderEventLoggers from "./ProviderEventLoggers.ts";
37import { ProviderInstanceRegistryHydrationLive } from "./ProviderInstanceRegistryHydration.ts";37import { ProviderInstanceRegistryHydrationLive } from "./ProviderInstanceRegistryHydration.ts";
38import {38import {
39 haveProvidersChanged,39 haveProvidersChanged,
...@@ -42,12 +42,12 @@ import {...@@ -42,12 +42,12 @@ import {
42 ProviderRegistryLive,42 ProviderRegistryLive,
43 selectProvidersByKind,43 selectProvidersByKind,
44} from "./ProviderRegistry.ts";44} from "./ProviderRegistry.ts";
45import { ServerConfig } from "../../config.ts";45import * as ServerConfig from "../../config.ts";
46import { ServerSettingsService, type ServerSettingsShape } from "../../serverSettings.ts";46import * as ServerSettingsModule from "../../serverSettings.ts";
47import { readProviderStatusCache, resolveProviderStatusCachePath } from "../providerStatusCache.ts";47import { readProviderStatusCache, resolveProviderStatusCachePath } from "../providerStatusCache.ts";
48import type { ProviderInstance } from "../ProviderDriver.ts";48import type { ProviderInstance } from "../ProviderDriver.ts";
49import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts";49import * as ProviderInstanceRegistry from "../Services/ProviderInstanceRegistry.ts";
50import { ProviderRegistry } from "../Services/ProviderRegistry.ts";50import * as ProviderRegistry from "../Services/ProviderRegistry.ts";
51import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts";51import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts";
52const decodeServerSettings = Schema.decodeSync(ServerSettings);52const decodeServerSettings = Schema.decodeSync(ServerSettings);
53const encodeServerSettings = Schema.encodeSync(ServerSettings);53const encodeServerSettings = Schema.encodeSync(ServerSettings);
...@@ -294,11 +294,11 @@ function makeMutableServerSettingsService(...@@ -294,11 +294,11 @@ function makeMutableServerSettingsService(
294 get streamChanges() {294 get streamChanges() {
295 return Stream.fromPubSub(changes);295 return Stream.fromPubSub(changes);
296 },296 },
297 } satisfies ServerSettingsShape;297 } satisfies ServerSettingsModule.ServerSettingsService["Service"];
298 });298 });
299}299}
300300
301it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), TestHttpClientLive))(301it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), TestHttpClientLive))(
302 "ProviderRegistry",302 "ProviderRegistry",
303 (it) => {303 (it) => {
304 describe("checkCodexProviderStatus", () => {304 describe("checkCodexProviderStatus", () => {
...@@ -636,14 +636,17 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -636,14 +636,17 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
636 adapter: {} as ProviderInstance["adapter"],636 adapter: {} as ProviderInstance["adapter"],
637 textGeneration: {} as ProviderInstance["textGeneration"],637 textGeneration: {} as ProviderInstance["textGeneration"],
638 } satisfies ProviderInstance;638 } satisfies ProviderInstance;
639 const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, {639 const instanceRegistryLayer = Layer.succeed(
640 getInstance: (instanceId) =>640 ProviderInstanceRegistry.ProviderInstanceRegistry,
641 Effect.succeed(instanceId === codexInstanceId ? instance : undefined),641 {
642 listInstances: Effect.succeed([instance]),642 getInstance: (instanceId) =>
643 listUnavailable: Effect.succeed([]),643 Effect.succeed(instanceId === codexInstanceId ? instance : undefined),
644 streamChanges: Stream.empty,644 listInstances: Effect.succeed([instance]),
645 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),645 listUnavailable: Effect.succeed([]),
646 });646 streamChanges: Stream.empty,
647 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),
648 },
649 );
647 const scope = yield* Scope.make();650 const scope = yield* Scope.make();
648 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));651 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
649 const runtimeServices = yield* Layer.build(652 const runtimeServices = yield* Layer.build(
...@@ -658,7 +661,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -658,7 +661,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
658 ),661 ),
659 ).pipe(Scope.provide(scope));662 ).pipe(Scope.provide(scope));
660 yield* Effect.gen(function* () {663 yield* Effect.gen(function* () {
661 const registry = yield* ProviderRegistry;664 const registry = yield* ProviderRegistry.ProviderRegistry;
662 assert.deepStrictEqual(yield* registry.getProviders, [initialProvider]);665 assert.deepStrictEqual(yield* registry.getProviders, [initialProvider]);
663 assert.strictEqual(yield* Ref.get(refreshCalls), 0);666 assert.strictEqual(yield* Ref.get(refreshCalls), 0);
664 }).pipe(Effect.provide(runtimeServices));667 }).pipe(Effect.provide(runtimeServices));
...@@ -786,16 +789,19 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -786,16 +789,19 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
786 adapter: {} as ProviderInstance["adapter"],789 adapter: {} as ProviderInstance["adapter"],
787 textGeneration: {} as ProviderInstance["textGeneration"],790 textGeneration: {} as ProviderInstance["textGeneration"],
788 } satisfies ProviderInstance;791 } satisfies ProviderInstance;
789 const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, {792 const instanceRegistryLayer = Layer.succeed(
790 getInstance: (instanceId) =>793 ProviderInstanceRegistry.ProviderInstanceRegistry,
791 Effect.succeed(instanceId === cursorInstanceId ? instance : undefined),794 {
792 listInstances: Effect.succeed([instance]),795 getInstance: (instanceId) =>
793 listUnavailable: Effect.succeed([]),796 Effect.succeed(instanceId === cursorInstanceId ? instance : undefined),
794 streamChanges: Stream.empty,797 listInstances: Effect.succeed([instance]),
795 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>798 listUnavailable: Effect.succeed([]),
796 PubSub.subscribe(pubsub),799 streamChanges: Stream.empty,
797 ),800 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>
798 });801 PubSub.subscribe(pubsub),
802 ),
803 },
804 );
799 const scope = yield* Scope.make();805 const scope = yield* Scope.make();
800 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));806 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
801 const runtimeServices = yield* Layer.build(807 const runtimeServices = yield* Layer.build(
...@@ -811,8 +817,8 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -811,8 +817,8 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
811 ).pipe(Scope.provide(scope));817 ).pipe(Scope.provide(scope));
812818
813 yield* Effect.gen(function* () {819 yield* Effect.gen(function* () {
814 const registry = yield* ProviderRegistry;820 const registry = yield* ProviderRegistry.ProviderRegistry;
815 const config = yield* ServerConfig;821 const config = yield* ServerConfig.ServerConfig;
816 const filePath = yield* resolveProviderStatusCachePath({822 const filePath = yield* resolveProviderStatusCachePath({
817 cacheDir: config.providerStatusCacheDir,823 cacheDir: config.providerStatusCacheDir,
818 instanceId: cursorInstanceId,824 instanceId: cursorInstanceId,
...@@ -880,16 +886,19 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -880,16 +886,19 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
880 adapter: {} as ProviderInstance["adapter"],886 adapter: {} as ProviderInstance["adapter"],
881 textGeneration: {} as ProviderInstance["textGeneration"],887 textGeneration: {} as ProviderInstance["textGeneration"],
882 } satisfies ProviderInstance;888 } satisfies ProviderInstance;
883 const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, {889 const instanceRegistryLayer = Layer.succeed(
884 getInstance: (instanceId) =>890 ProviderInstanceRegistry.ProviderInstanceRegistry,
885 Effect.succeed(instanceId === codexInstanceId ? instance : undefined),891 {
886 listInstances: Effect.succeed([instance]),892 getInstance: (instanceId) =>
887 listUnavailable: Effect.succeed([]),893 Effect.succeed(instanceId === codexInstanceId ? instance : undefined),
888 streamChanges: Stream.empty,894 listInstances: Effect.succeed([instance]),
889 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>895 listUnavailable: Effect.succeed([]),
890 PubSub.subscribe(pubsub),896 streamChanges: Stream.empty,
891 ),897 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>
892 });898 PubSub.subscribe(pubsub),
899 ),
900 },
901 );
893 const scope = yield* Scope.make();902 const scope = yield* Scope.make();
894 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));903 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
895 const runtimeServices = yield* Layer.build(904 const runtimeServices = yield* Layer.build(
...@@ -905,7 +914,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -905,7 +914,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
905 ).pipe(Scope.provide(scope));914 ).pipe(Scope.provide(scope));
906915
907 yield* Effect.gen(function* () {916 yield* Effect.gen(function* () {
908 const registry = yield* ProviderRegistry;917 const registry = yield* ProviderRegistry.ProviderRegistry;
909918
910 assert.deepStrictEqual(yield* registry.getProviders, [cachedProvider]);919 assert.deepStrictEqual(yield* registry.getProviders, [cachedProvider]);
911 assert.deepStrictEqual(yield* registry.refresh(codexDriver), [cachedProvider]);920 assert.deepStrictEqual(yield* registry.refresh(codexDriver), [cachedProvider]);
...@@ -975,25 +984,28 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -975,25 +984,28 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
975 const instancesRef = yield* Ref.make<ReadonlyArray<ProviderInstance>>([codexInstance]);984 const instancesRef = yield* Ref.make<ReadonlyArray<ProviderInstance>>([codexInstance]);
976 const failNextList = yield* Ref.make(false);985 const failNextList = yield* Ref.make(false);
977 const wait = () => Effect.yieldNow;986 const wait = () => Effect.yieldNow;
978 const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, {987 const instanceRegistryLayer = Layer.succeed(
979 getInstance: (instanceId) =>988 ProviderInstanceRegistry.ProviderInstanceRegistry,
980 Ref.get(instancesRef).pipe(989 {
981 Effect.map((instances) =>990 getInstance: (instanceId) =>
982 instances.find((instance) => instance.instanceId === instanceId),991 Ref.get(instancesRef).pipe(
992 Effect.map((instances) =>
993 instances.find((instance) => instance.instanceId === instanceId),
994 ),
983 ),995 ),
984 ),996 listInstances: Effect.gen(function* () {
985 listInstances: Effect.gen(function* () {997 const shouldFail = yield* Ref.get(failNextList);
986 const shouldFail = yield* Ref.get(failNextList);998 if (shouldFail) {
987 if (shouldFail) {999 yield* Ref.set(failNextList, false);
988 yield* Ref.set(failNextList, false);1000 return yield* Effect.die(new Error("simulated registry list failure"));
989 return yield* Effect.die(new Error("simulated registry list failure"));1001 }
990 }1002 return yield* Ref.get(instancesRef);
991 return yield* Ref.get(instancesRef);1003 }),
992 }),1004 listUnavailable: Effect.succeed([]),
993 listUnavailable: Effect.succeed([]),1005 streamChanges: Stream.fromPubSub(changes),
994 streamChanges: Stream.fromPubSub(changes),1006 subscribeChanges: PubSub.subscribe(changes),
995 subscribeChanges: PubSub.subscribe(changes),1007 },
996 });1008 );
997 const scope = yield* Scope.make();1009 const scope = yield* Scope.make();
998 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));1010 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
999 const runtimeServices = yield* Layer.build(1011 const runtimeServices = yield* Layer.build(
...@@ -1009,7 +1021,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -1009,7 +1021,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
1009 ).pipe(Scope.provide(scope));1021 ).pipe(Scope.provide(scope));
10101022
1011 yield* Effect.gen(function* () {1023 yield* Effect.gen(function* () {
1012 const registry = yield* ProviderRegistry;1024 const registry = yield* ProviderRegistry.ProviderRegistry;
1013 assert.deepStrictEqual(yield* registry.getProviders, [codexProvider]);1025 assert.deepStrictEqual(yield* registry.getProviders, [codexProvider]);
10141026
1015 yield* Ref.set(failNextList, true);1027 yield* Ref.set(failNextList, true);
...@@ -1092,15 +1104,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -1092,15 +1104,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
1092 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));1104 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
1093 const providerRegistryLayer = ProviderRegistryLive.pipe(1105 const providerRegistryLayer = ProviderRegistryLive.pipe(
1094 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),1106 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),
1095 Layer.provideMerge(Layer.succeed(ServerSettingsService, serverSettings)),1107 Layer.provideMerge(
1108 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
1109 ),
1096 Layer.provideMerge(1110 Layer.provideMerge(
1097 ServerConfig.layerTest(process.cwd(), {1111 ServerConfig.layerTest(process.cwd(), {
1098 prefix: "t3-provider-registry-",1112 prefix: "t3-provider-registry-",
1099 }),1113 }),
1100 ),1114 ),
1101 Layer.provideMerge(TestHttpClientLive),1115 Layer.provideMerge(TestHttpClientLive),
1102 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),1116 Layer.provideMerge(
1103 Layer.provideMerge(OpenCodeRuntimeLive),1117 Layer.succeed(
1118 ProviderEventLoggers.ProviderEventLoggers,
1119 ProviderEventLoggers.NoOpProviderEventLoggers,
1120 ),
1121 ),
1122 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
1104 // NO spawner mock — `ChildProcessSpawner` is supplied by the1123 // NO spawner mock — `ChildProcessSpawner` is supplied by the
1105 // outer `NodeServices.layer` on `it.layer(...)` and will1124 // outer `NodeServices.layer` on `it.layer(...)` and will
1106 // genuinely spawn a subprocess. The missing-binary ENOENT is1125 // genuinely spawn a subprocess. The missing-binary ENOENT is
...@@ -1112,7 +1131,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -1112,7 +1131,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
1112 );1131 );
11131132
1114 yield* Effect.gen(function* () {1133 yield* Effect.gen(function* () {
1115 const registry = yield* ProviderRegistry;1134 const registry = yield* ProviderRegistry.ProviderRegistry;
1116 let providers = yield* registry.getProviders;1135 let providers = yield* registry.getProviders;
1117 for (1136 for (
1118 let attempts = 0;1137 let attempts = 0;
...@@ -1177,15 +1196,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -1177,15 +1196,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
1177 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));1196 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
1178 const providerRegistryLayer = ProviderRegistryLive.pipe(1197 const providerRegistryLayer = ProviderRegistryLive.pipe(
1179 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),1198 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),
1180 Layer.provideMerge(Layer.succeed(ServerSettingsService, serverSettings)),1199 Layer.provideMerge(
1200 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
1201 ),
1181 Layer.provideMerge(1202 Layer.provideMerge(
1182 ServerConfig.layerTest(process.cwd(), {1203 ServerConfig.layerTest(process.cwd(), {
1183 prefix: "t3-provider-registry-",1204 prefix: "t3-provider-registry-",
1184 }),1205 }),
1185 ),1206 ),
1186 Layer.provideMerge(TestHttpClientLive),1207 Layer.provideMerge(TestHttpClientLive),
1187 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),1208 Layer.provideMerge(
1188 Layer.provideMerge(OpenCodeRuntimeLive),1209 Layer.succeed(
1210 ProviderEventLoggers.ProviderEventLoggers,
1211 ProviderEventLoggers.NoOpProviderEventLoggers,
1212 ),
1213 ),
1214 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
1189 Layer.updateService(ChildProcessSpawner.ChildProcessSpawner, (spawner) =>1215 Layer.updateService(ChildProcessSpawner.ChildProcessSpawner, (spawner) =>
1190 ChildProcessSpawner.make((command) => {1216 ChildProcessSpawner.make((command) => {
1191 spawnedCommands.push((command as { readonly command: string }).command);1217 spawnedCommands.push((command as { readonly command: string }).command);
...@@ -1199,7 +1225,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -1199,7 +1225,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
1199 );1225 );
12001226
1201 yield* Effect.gen(function* () {1227 yield* Effect.gen(function* () {
1202 const registry = yield* ProviderRegistry;1228 const registry = yield* ProviderRegistry.ProviderRegistry;
1203 // Boot-time probe: the default codex instance is enabled with1229 // Boot-time probe: the default codex instance is enabled with
1204 // `firstMissing`, so the real spawner yields ENOENT and the1230 // `firstMissing`, so the real spawner yields ENOENT and the
1205 // snapshot should be `status: "error"`.1231 // snapshot should be `status: "error"`.
...@@ -1291,15 +1317,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -1291,15 +1317,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
1291 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));1317 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
1292 const providerRegistryLayer = ProviderRegistryLive.pipe(1318 const providerRegistryLayer = ProviderRegistryLive.pipe(
1293 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),1319 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),
1294 Layer.provideMerge(Layer.succeed(ServerSettingsService, serverSettings)),1320 Layer.provideMerge(
1321 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
1322 ),
1295 Layer.provideMerge(1323 Layer.provideMerge(
1296 ServerConfig.layerTest(process.cwd(), {1324 ServerConfig.layerTest(process.cwd(), {
1297 prefix: "t3-provider-registry-",1325 prefix: "t3-provider-registry-",
1298 }),1326 }),
1299 ),1327 ),
1300 Layer.provideMerge(TestHttpClientLive),1328 Layer.provideMerge(TestHttpClientLive),
1301 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),1329 Layer.provideMerge(
1302 Layer.provideMerge(OpenCodeRuntimeLive),1330 Layer.succeed(
1331 ProviderEventLoggers.ProviderEventLoggers,
1332 ProviderEventLoggers.NoOpProviderEventLoggers,
1333 ),
1334 ),
1335 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
1303 Layer.provideMerge(NodeServices.layer),1336 Layer.provideMerge(NodeServices.layer),
1304 );1337 );
1305 const runtimeServices = yield* Layer.build(providerRegistryLayer).pipe(1338 const runtimeServices = yield* Layer.build(providerRegistryLayer).pipe(
...@@ -1307,7 +1340,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -1307,7 +1340,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
1307 );1340 );
13081341
1309 yield* Effect.gen(function* () {1342 yield* Effect.gen(function* () {
1310 const registry = yield* ProviderRegistry;1343 const registry = yield* ProviderRegistry.ProviderRegistry;
1311 const providers = yield* registry.getProviders;1344 const providers = yield* registry.getProviders;
1312 const ghost = providers.find((provider) => provider.instanceId === "ghost_main");1345 const ghost = providers.find((provider) => provider.instanceId === "ghost_main");
13131346
...@@ -1345,15 +1378,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -1345,15 +1378,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
1345 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));1378 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
1346 const providerRegistryLayer = ProviderRegistryLive.pipe(1379 const providerRegistryLayer = ProviderRegistryLive.pipe(
1347 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),1380 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),
1348 Layer.provideMerge(Layer.succeed(ServerSettingsService, serverSettings)),1381 Layer.provideMerge(
1382 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
1383 ),
1349 Layer.provideMerge(1384 Layer.provideMerge(
1350 ServerConfig.layerTest(process.cwd(), {1385 ServerConfig.layerTest(process.cwd(), {
1351 prefix: "t3-provider-registry-",1386 prefix: "t3-provider-registry-",
1352 }),1387 }),
1353 ),1388 ),
1354 Layer.provideMerge(TestHttpClientLive),1389 Layer.provideMerge(TestHttpClientLive),
1355 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),1390 Layer.provideMerge(
1356 Layer.provideMerge(OpenCodeRuntimeLive),1391 Layer.succeed(
1392 ProviderEventLoggers.ProviderEventLoggers,
1393 ProviderEventLoggers.NoOpProviderEventLoggers,
1394 ),
1395 ),
1396 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
1357 Layer.provideMerge(1397 Layer.provideMerge(
1358 mockCommandSpawnerLayer((command, args) => {1398 mockCommandSpawnerLayer((command, args) => {
1359 if (command === "agent") {1399 if (command === "agent") {
...@@ -1380,13 +1420,13 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -1380,13 +1420,13 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
1380 );1420 );
1381 const runtimeServices = yield* Layer.build(1421 const runtimeServices = yield* Layer.build(
1382 Layer.mergeAll(1422 Layer.mergeAll(
1383 Layer.succeed(ServerSettingsService, serverSettings),1423 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
1384 providerRegistryLayer,1424 providerRegistryLayer,
1385 ),1425 ),
1386 ).pipe(Scope.provide(scope));1426 ).pipe(Scope.provide(scope));
13871427
1388 yield* Effect.gen(function* () {1428 yield* Effect.gen(function* () {
1389 const registry = yield* ProviderRegistry;1429 const registry = yield* ProviderRegistry.ProviderRegistry;
1390 const providers = yield* registry.getProviders;1430 const providers = yield* registry.getProviders;
1391 const cursorProvider = providers.find(1431 const cursorProvider = providers.find(
1392 (provider) => provider.instanceId === ProviderInstanceId.make("cursor"),1432 (provider) => provider.instanceId === ProviderInstanceId.make("cursor"),
apps/server/src/provider/providerUpdateSettings.ts+2-2
...@@ -3,7 +3,7 @@ import * as Effect from "effect/Effect";...@@ -3,7 +3,7 @@ import * as Effect from "effect/Effect";
3import * as Equal from "effect/Equal";3import * as Equal from "effect/Equal";
4import * as Stream from "effect/Stream";4import * as Stream from "effect/Stream";
55
6import type { ServerSettingsShape } from "../serverSettings.ts";6import type * as ServerSettingsModule from "../serverSettings.ts";
77
8export interface ProviderSnapshotSettings<Settings> {8export interface ProviderSnapshotSettings<Settings> {
9 readonly provider: Settings;9 readonly provider: Settings;
...@@ -29,7 +29,7 @@ export function haveProviderSnapshotSettingsChanged<Settings>(...@@ -29,7 +29,7 @@ export function haveProviderSnapshotSettingsChanged<Settings>(
2929
30export function makeProviderSnapshotSettingsSource<Settings>(30export function makeProviderSnapshotSettingsSource<Settings>(
31 provider: Settings,31 provider: Settings,
32 serverSettings: ServerSettingsShape,32 serverSettings: ServerSettingsModule.ServerSettingsService["Service"],
33): {33): {
34 readonly getSettings: Effect.Effect<ProviderSnapshotSettings<Settings>, ServerSettingsError>;34 readonly getSettings: Effect.Effect<ProviderSnapshotSettings<Settings>, ServerSettingsError>;
35 readonly streamSettings: Stream.Stream<ProviderSnapshotSettings<Settings>>;35 readonly streamSettings: Stream.Stream<ProviderSnapshotSettings<Settings>>;
apps/server/src/server.test.ts+95-100
...@@ -69,56 +69,30 @@ import { vi } from "vite-plus/test";...@@ -69,56 +69,30 @@ import { vi } from "vite-plus/test";
6969
70const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z");70const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z");
7171
72import type { ServerConfigShape } from "./config.ts";72import * as ServerConfig from "./config.ts";
73import { deriveServerPaths, ServerConfig } from "./config.ts";
74import { makeRoutesLayer } from "./server.ts";73import { makeRoutesLayer } from "./server.ts";
75import {74import * as CheckpointDiffQuery from "./checkpointing/Services/CheckpointDiffQuery.ts";
76 CheckpointDiffQuery,75import * as GitManager from "./git/GitManager.ts";
77 type CheckpointDiffQueryShape,76import * as Keybindings from "./keybindings.ts";
78} from "./checkpointing/Services/CheckpointDiffQuery.ts";
79import { GitManager, type GitManagerShape } from "./git/GitManager.ts";
80import { Keybindings, type KeybindingsShape } from "./keybindings.ts";
81import * as ExternalLauncher from "./process/externalLauncher.ts";77import * as ExternalLauncher from "./process/externalLauncher.ts";
82import {78import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts";
83 OrchestrationEngineService,
84 type OrchestrationEngineShape,
85} from "./orchestration/Services/OrchestrationEngine.ts";
86import { OrchestrationListenerCallbackError } from "./orchestration/Errors.ts";79import { OrchestrationListenerCallbackError } from "./orchestration/Errors.ts";
87import {80import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts";
88 ProjectionSnapshotQuery,
89 type ProjectionSnapshotQueryShape,
90} from "./orchestration/Services/ProjectionSnapshotQuery.ts";
91import { SqlitePersistenceMemory } from "./persistence/Layers/Sqlite.ts";81import { SqlitePersistenceMemory } from "./persistence/Layers/Sqlite.ts";
92import { PersistenceSqlError } from "./persistence/Errors.ts";82import { PersistenceSqlError } from "./persistence/Errors.ts";
93import {83import * as ProviderRegistry from "./provider/Services/ProviderRegistry.ts";
94 ProviderRegistry,
95 type ProviderRegistryShape,
96} from "./provider/Services/ProviderRegistry.ts";
97import { makeManualOnlyProviderMaintenanceCapabilities } from "./provider/providerMaintenance.ts";84import { makeManualOnlyProviderMaintenanceCapabilities } from "./provider/providerMaintenance.ts";
98import { ServerLifecycleEvents, type ServerLifecycleEventsShape } from "./serverLifecycleEvents.ts";85import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";
99import { ServerRuntimeStartup, type ServerRuntimeStartupShape } from "./serverRuntimeStartup.ts";86import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts";
100import { ServerSettingsService, type ServerSettingsShape } from "./serverSettings.ts";87import * as ServerSettings from "./serverSettings.ts";
101import { TerminalManager, type TerminalManagerShape } from "./terminal/Services/Manager.ts";88import * as TerminalManager from "./terminal/Services/Manager.ts";
102import * as PreviewManager from "./preview/Manager.ts";89import * as PreviewManager from "./preview/Manager.ts";
103import * as PortScanner from "./preview/PortScanner.ts";90import * as PortScanner from "./preview/PortScanner.ts";
104import {91import * as BrowserTraceCollector from "./observability/Services/BrowserTraceCollector.ts";
105 BrowserTraceCollector,
106 type BrowserTraceCollectorShape,
107} from "./observability/Services/BrowserTraceCollector.ts";
108import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts";92import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts";
109import {93import * as ProjectSetupScriptRunner from "./project/Services/ProjectSetupScriptRunner.ts";
110 ProjectSetupScriptRunner,94import * as RepositoryIdentityResolver from "./project/Services/RepositoryIdentityResolver.ts";
111 ProjectSetupScriptRunnerError,95import * as ServerEnvironment from "./environment/Services/ServerEnvironment.ts";
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";
122import * as WorkspaceEntries from "./workspace/WorkspaceEntries.ts";96import * as WorkspaceEntries from "./workspace/WorkspaceEntries.ts";
123import { WorkspaceFileSystemLive } from "./workspace/Layers/WorkspaceFileSystem.ts";97import { WorkspaceFileSystemLive } from "./workspace/Layers/WorkspaceFileSystem.ts";
124import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths.ts";98import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths.ts";
...@@ -132,10 +106,7 @@ import * as ReviewService from "./review/ReviewService.ts";...@@ -132,10 +106,7 @@ import * as ReviewService from "./review/ReviewService.ts";
132import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts";106import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts";
133import * as ServerSecretStore from "./auth/ServerSecretStore.ts";107import * as ServerSecretStore from "./auth/ServerSecretStore.ts";
134import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";108import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
135import {109import * as CloudManagedEndpointRuntime from "./cloud/ManagedEndpointRuntime.ts";
136 CloudManagedEndpointRuntime,
137 type CloudManagedEndpointRuntimeShape,
138} from "./cloud/ManagedEndpointRuntime.ts";
139import * as CloudCliTokenManager from "./cloud/CliTokenManager.ts";110import * as CloudCliTokenManager from "./cloud/CliTokenManager.ts";
140import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts";111import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts";
141import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts";112import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts";
...@@ -341,32 +312,40 @@ const makeBrowserOtlpPayload = (spanName: string) =>...@@ -341,32 +312,40 @@ const makeBrowserOtlpPayload = (spanName: string) =>
341 });312 });
342313
343const buildAppUnderTest = (options?: {314const buildAppUnderTest = (options?: {
344 config?: Partial<ServerConfigShape>;315 config?: Partial<ServerConfig.ServerConfig["Service"]>;
345 layers?: {316 layers?: {
346 keybindings?: Partial<KeybindingsShape>;317 keybindings?: Partial<Keybindings.Keybindings["Service"]>;
347 providerRegistry?: Partial<ProviderRegistryShape>;318 providerRegistry?: Partial<ProviderRegistry.ProviderRegistry["Service"]>;
348 serverSettings?: Partial<ServerSettingsShape>;319 serverSettings?: Partial<ServerSettings.ServerSettingsService["Service"]>;
349 externalLauncher?: Partial<ExternalLauncher.ExternalLauncherShape>;320 externalLauncher?: Partial<ExternalLauncher.ExternalLauncher["Service"]>;
350 vcsDriver?: Partial<VcsDriver.VcsDriverShape>;321 vcsDriver?: Partial<VcsDriver.VcsDriver["Service"]>;
351 vcsDriverRegistry?: Partial<VcsDriverRegistry.VcsDriverRegistryShape>;322 vcsDriverRegistry?: Partial<VcsDriverRegistry.VcsDriverRegistry["Service"]>;
352 gitVcsDriver?: Partial<GitVcsDriver.GitVcsDriverShape>;323 gitVcsDriver?: Partial<GitVcsDriver.GitVcsDriver["Service"]>;
353 gitManager?: Partial<GitManagerShape>;324 gitManager?: Partial<GitManager.GitManager["Service"]>;
354 sourceControlRepositoryService?: Partial<SourceControlRepositoryService.SourceControlRepositoryServiceShape>;325 sourceControlRepositoryService?: Partial<
355 reviewService?: Partial<ReviewService.ReviewServiceShape>;326 SourceControlRepositoryService.SourceControlRepositoryService["Service"]
356 vcsStatusBroadcaster?: Partial<VcsStatusBroadcaster.VcsStatusBroadcasterShape>;327 >;
357 projectSetupScriptRunner?: Partial<ProjectSetupScriptRunnerShape>;328 reviewService?: Partial<ReviewService.ReviewService["Service"]>;
358 terminalManager?: Partial<TerminalManagerShape>;329 vcsStatusBroadcaster?: Partial<VcsStatusBroadcaster.VcsStatusBroadcaster["Service"]>;
359 orchestrationEngine?: Partial<OrchestrationEngineShape>;330 projectSetupScriptRunner?: Partial<
360 projectionSnapshotQuery?: Partial<ProjectionSnapshotQueryShape>;331 ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]
361 checkpointDiffQuery?: Partial<CheckpointDiffQueryShape>;332 >;
362 browserTraceCollector?: Partial<BrowserTraceCollectorShape>;333 terminalManager?: Partial<TerminalManager.TerminalManager["Service"]>;
363 serverLifecycleEvents?: Partial<ServerLifecycleEventsShape>;334 orchestrationEngine?: Partial<OrchestrationEngine.OrchestrationEngineService["Service"]>;
364 serverRuntimeStartup?: Partial<ServerRuntimeStartupShape>;335 projectionSnapshotQuery?: Partial<ProjectionSnapshotQuery.ProjectionSnapshotQuery["Service"]>;
365 serverEnvironment?: Partial<ServerEnvironmentShape>;336 checkpointDiffQuery?: Partial<CheckpointDiffQuery.CheckpointDiffQuery["Service"]>;
366 repositoryIdentityResolver?: Partial<RepositoryIdentityResolverShape>;337 browserTraceCollector?: Partial<BrowserTraceCollector.BrowserTraceCollector["Service"]>;
367 cloudManagedEndpointRuntime?: Partial<CloudManagedEndpointRuntimeShape>;338 serverLifecycleEvents?: Partial<ServerLifecycleEvents.ServerLifecycleEvents["Service"]>;
368 relayClient?: Partial<RelayClient.RelayClientShape>;339 serverRuntimeStartup?: Partial<ServerRuntimeStartup.ServerRuntimeStartup["Service"]>;
369 cloudCliTokenManager?: Partial<CloudCliTokenManager.CloudCliTokenManagerShape>;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"]>;
370 };349 };
371}) =>350}) =>
372 Effect.gen(function* () {351 Effect.gen(function* () {
...@@ -374,8 +353,8 @@ const buildAppUnderTest = (options?: {...@@ -374,8 +353,8 @@ const buildAppUnderTest = (options?: {
374 const tempBaseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-router-test-" });353 const tempBaseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-router-test-" });
375 const baseDir = options?.config?.baseDir ?? tempBaseDir;354 const baseDir = options?.config?.baseDir ?? tempBaseDir;
376 const devUrl = options?.config?.devUrl;355 const devUrl = options?.config?.devUrl;
377 const derivedPaths = yield* deriveServerPaths(baseDir, devUrl);356 const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, devUrl);
378 const config: ServerConfigShape = {357 const config: ServerConfig.ServerConfig["Service"] = {
379 logLevel: "Info",358 logLevel: "Info",
380 traceMinLevel: "Info",359 traceMinLevel: "Info",
381 traceTimingEnabled: true,360 traceTimingEnabled: true,
...@@ -403,8 +382,8 @@ const buildAppUnderTest = (options?: {...@@ -403,8 +382,8 @@ const buildAppUnderTest = (options?: {
403 tailscaleServePort: 443,382 tailscaleServePort: 443,
404 ...options?.config,383 ...options?.config,
405 };384 };
406 const layerConfig = Layer.succeed(ServerConfig, config);385 const layerConfig = ServerConfig.layer(config);
407 const defaultVcsDriver: VcsDriver.VcsDriverShape = {386 const defaultVcsDriver: VcsDriver.VcsDriver["Service"] = {
408 capabilities: {387 capabilities: {
409 kind: "git",388 kind: "git",
410 supportsWorktrees: true,389 supportsWorktrees: true,
...@@ -502,7 +481,7 @@ const buildAppUnderTest = (options?: {...@@ -502,7 +481,7 @@ const buildAppUnderTest = (options?: {
502 const gitVcsDriverLayer = Layer.mock(GitVcsDriver.GitVcsDriver)({481 const gitVcsDriverLayer = Layer.mock(GitVcsDriver.GitVcsDriver)({
503 ...options?.layers?.gitVcsDriver,482 ...options?.layers?.gitVcsDriver,
504 });483 });
505 const gitManagerLayer = Layer.mock(GitManager)({484 const gitManagerLayer = Layer.mock(GitManager.GitManager)({
506 ...options?.layers?.gitManager,485 ...options?.layers?.gitManager,
507 });486 });
508 const workspaceEntriesLayer = WorkspaceEntries.layer.pipe(487 const workspaceEntriesLayer = WorkspaceEntries.layer.pipe(
...@@ -545,7 +524,7 @@ const buildAppUnderTest = (options?: {...@@ -545,7 +524,7 @@ const buildAppUnderTest = (options?: {
545 disableLogger: true,524 disableLogger: true,
546 }).pipe(525 }).pipe(
547 Layer.provide(526 Layer.provide(
548 Layer.mock(Keybindings)({527 Layer.mock(Keybindings.Keybindings)({
549 loadConfigState: Effect.succeed({528 loadConfigState: Effect.succeed({
550 keybindings: [],529 keybindings: [],
551 issues: [],530 issues: [],
...@@ -555,7 +534,7 @@ const buildAppUnderTest = (options?: {...@@ -555,7 +534,7 @@ const buildAppUnderTest = (options?: {
555 }),534 }),
556 ),535 ),
557 Layer.provide(536 Layer.provide(
558 Layer.mock(ProviderRegistry)({537 Layer.mock(ProviderRegistry.ProviderRegistry)({
559 getProviders: Effect.succeed([]),538 getProviders: Effect.succeed([]),
560 refresh: () => Effect.succeed([]),539 refresh: () => Effect.succeed([]),
561 refreshInstance: () => Effect.succeed([]),540 refreshInstance: () => Effect.succeed([]),
...@@ -569,7 +548,7 @@ const buildAppUnderTest = (options?: {...@@ -569,7 +548,7 @@ const buildAppUnderTest = (options?: {
569 }),548 }),
570 ),549 ),
571 Layer.provide(550 Layer.provide(
572 Layer.mock(ServerSettingsService)({551 Layer.mock(ServerSettings.ServerSettingsService)({
573 start: Effect.void,552 start: Effect.void,
574 ready: Effect.void,553 ready: Effect.void,
575 getSettings: Effect.succeed(DEFAULT_SERVER_SETTINGS),554 getSettings: Effect.succeed(DEFAULT_SERVER_SETTINGS),
...@@ -658,13 +637,13 @@ const buildAppUnderTest = (options?: {...@@ -658,13 +637,13 @@ const buildAppUnderTest = (options?: {
658 ),637 ),
659 Layer.provideMerge(vcsStatusBroadcasterLayer),638 Layer.provideMerge(vcsStatusBroadcasterLayer),
660 Layer.provide(639 Layer.provide(
661 Layer.mock(ProjectSetupScriptRunner)({640 Layer.mock(ProjectSetupScriptRunner.ProjectSetupScriptRunner)({
662 runForThread: () => Effect.succeed({ status: "no-script" as const }),641 runForThread: () => Effect.succeed({ status: "no-script" as const }),
663 ...options?.layers?.projectSetupScriptRunner,642 ...options?.layers?.projectSetupScriptRunner,
664 }),643 }),
665 ),644 ),
666 Layer.provide(645 Layer.provide(
667 Layer.mock(TerminalManager)({646 Layer.mock(TerminalManager.TerminalManager)({
668 ...options?.layers?.terminalManager,647 ...options?.layers?.terminalManager,
669 }),648 }),
670 ),649 ),
...@@ -692,7 +671,7 @@ const buildAppUnderTest = (options?: {...@@ -692,7 +671,7 @@ const buildAppUnderTest = (options?: {
692 ),671 ),
693 ),672 ),
694 Layer.provide(673 Layer.provide(
695 Layer.mock(OrchestrationEngineService)({674 Layer.mock(OrchestrationEngine.OrchestrationEngineService)({
696 readEvents: () => Stream.empty,675 readEvents: () => Stream.empty,
697 dispatch: () => Effect.succeed({ sequence: 0 }),676 dispatch: () => Effect.succeed({ sequence: 0 }),
698 streamDomainEvents: Stream.empty,677 streamDomainEvents: Stream.empty,
...@@ -700,7 +679,7 @@ const buildAppUnderTest = (options?: {...@@ -700,7 +679,7 @@ const buildAppUnderTest = (options?: {
700 }),679 }),
701 ),680 ),
702 Layer.provide(681 Layer.provide(
703 Layer.mock(ProjectionSnapshotQuery)({682 Layer.mock(ProjectionSnapshotQuery.ProjectionSnapshotQuery)({
704 getCommandReadModel: () => Effect.succeed(makeDefaultOrchestrationReadModel()),683 getCommandReadModel: () => Effect.succeed(makeDefaultOrchestrationReadModel()),
705 getSnapshot: () => Effect.succeed(makeDefaultOrchestrationReadModel()),684 getSnapshot: () => Effect.succeed(makeDefaultOrchestrationReadModel()),
706 getShellSnapshot: () =>685 getShellSnapshot: () =>
...@@ -729,7 +708,7 @@ const buildAppUnderTest = (options?: {...@@ -729,7 +708,7 @@ const buildAppUnderTest = (options?: {
729 }),708 }),
730 ),709 ),
731 Layer.provide(710 Layer.provide(
732 Layer.mock(CheckpointDiffQuery)({711 Layer.mock(CheckpointDiffQuery.CheckpointDiffQuery)({
733 getTurnDiff: () =>712 getTurnDiff: () =>
734 Effect.succeed({713 Effect.succeed({
735 threadId: defaultThreadId,714 threadId: defaultThreadId,
...@@ -751,13 +730,13 @@ const buildAppUnderTest = (options?: {...@@ -751,13 +730,13 @@ const buildAppUnderTest = (options?: {
751730
752 const appLayer = servedRoutesLayer.pipe(731 const appLayer = servedRoutesLayer.pipe(
753 Layer.provide(732 Layer.provide(
754 Layer.mock(BrowserTraceCollector)({733 Layer.mock(BrowserTraceCollector.BrowserTraceCollector)({
755 record: () => Effect.void,734 record: () => Effect.void,
756 ...options?.layers?.browserTraceCollector,735 ...options?.layers?.browserTraceCollector,
757 }),736 }),
758 ),737 ),
759 Layer.provide(738 Layer.provide(
760 Layer.mock(ServerLifecycleEvents)({739 Layer.mock(ServerLifecycleEvents.ServerLifecycleEvents)({
761 publish: (event) => Effect.succeed({ ...(event as any), sequence: 1 }),740 publish: (event) => Effect.succeed({ ...(event as any), sequence: 1 }),
762 snapshot: Effect.succeed({ sequence: 0, events: [] }),741 snapshot: Effect.succeed({ sequence: 0, events: [] }),
763 stream: Stream.empty,742 stream: Stream.empty,
...@@ -765,7 +744,7 @@ const buildAppUnderTest = (options?: {...@@ -765,7 +744,7 @@ const buildAppUnderTest = (options?: {
765 }),744 }),
766 ),745 ),
767 Layer.provide(746 Layer.provide(
768 Layer.mock(ServerRuntimeStartup)({747 Layer.mock(ServerRuntimeStartup.ServerRuntimeStartup)({
769 awaitCommandReady: Effect.void,748 awaitCommandReady: Effect.void,
770 markHttpListening: Effect.void,749 markHttpListening: Effect.void,
771 enqueueCommand: (effect) => effect,750 enqueueCommand: (effect) => effect,
...@@ -773,22 +752,22 @@ const buildAppUnderTest = (options?: {...@@ -773,22 +752,22 @@ const buildAppUnderTest = (options?: {
773 }),752 }),
774 ),753 ),
775 Layer.provide(754 Layer.provide(
776 Layer.mock(ServerEnvironment)({755 Layer.mock(ServerEnvironment.ServerEnvironment)({
777 getEnvironmentId: Effect.succeed(testEnvironmentDescriptor.environmentId),756 getEnvironmentId: Effect.succeed(testEnvironmentDescriptor.environmentId),
778 getDescriptor: Effect.succeed(testEnvironmentDescriptor),757 getDescriptor: Effect.succeed(testEnvironmentDescriptor),
779 ...options?.layers?.serverEnvironment,758 ...options?.layers?.serverEnvironment,
780 }),759 }),
781 ),760 ),
782 Layer.provide(761 Layer.provide(
783 Layer.mock(RepositoryIdentityResolver)({762 Layer.mock(RepositoryIdentityResolver.RepositoryIdentityResolver)({
784 resolve: () => Effect.succeed(null),763 resolve: () => Effect.succeed(null),
785 ...options?.layers?.repositoryIdentityResolver,764 ...options?.layers?.repositoryIdentityResolver,
786 }),765 }),
787 ),766 ),
788 Layer.provide(767 Layer.provide(
789 Layer.succeed(768 Layer.succeed(
790 CloudManagedEndpointRuntime,769 CloudManagedEndpointRuntime.CloudManagedEndpointRuntime,
791 CloudManagedEndpointRuntime.of({770 CloudManagedEndpointRuntime.CloudManagedEndpointRuntime.of({
792 applyConfig: () => Effect.succeed({ status: "disabled" }),771 applyConfig: () => Effect.succeed({ status: "disabled" }),
793 ...options?.layers?.cloudManagedEndpointRuntime,772 ...options?.layers?.cloudManagedEndpointRuntime,
794 }),773 }),
...@@ -5938,14 +5917,14 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -5938,14 +5917,14 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
5938 }),5917 }),
5939 );5918 );
5940 const fetchRemote = vi.fn(5919 const fetchRemote = vi.fn(
5941 (_: Parameters<GitVcsDriver.GitVcsDriverShape["fetchRemote"]>[0]) =>5920 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["fetchRemote"]>[0]) =>
5942 Effect.sync(() => {5921 Effect.sync(() => {
5943 bootstrapGitOperations.push("fetch");5922 bootstrapGitOperations.push("fetch");
5944 }),5923 }),
5945 );5924 );
5946 const fetchedOriginCommit = "0123456789abcdef0123456789abcdef01234567";5925 const fetchedOriginCommit = "0123456789abcdef0123456789abcdef01234567";
5947 const resolveRemoteTrackingCommit = vi.fn(5926 const resolveRemoteTrackingCommit = vi.fn(
5948 (_: Parameters<GitVcsDriver.GitVcsDriverShape["resolveRemoteTrackingCommit"]>[0]) =>5927 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["resolveRemoteTrackingCommit"]>[0]) =>
5949 Effect.sync(() => {5928 Effect.sync(() => {
5950 bootstrapGitOperations.push("resolve-remote-commit");5929 bootstrapGitOperations.push("resolve-remote-commit");
5951 return {5930 return {
...@@ -5955,7 +5934,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -5955,7 +5934,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
5955 }),5934 }),
5956 );5935 );
5957 const createWorktree = vi.fn(5936 const createWorktree = vi.fn(
5958 (_: Parameters<GitVcsDriver.GitVcsDriverShape["createWorktree"]>[0]) =>5937 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["createWorktree"]>[0]) =>
5959 Effect.sync(() => {5938 Effect.sync(() => {
5960 bootstrapGitOperations.push("create-worktree");5939 bootstrapGitOperations.push("create-worktree");
5961 return {5940 return {
...@@ -5967,7 +5946,11 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -5967,7 +5946,11 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
5967 }),5946 }),
5968 );5947 );
5969 const runForThread = vi.fn(5948 const runForThread = vi.fn(
5970 (_: Parameters<ProjectSetupScriptRunnerShape["runForThread"]>[0]) =>5949 (
5950 _: Parameters<
5951 ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"]
5952 >[0],
5953 ) =>
5971 Effect.succeed({5954 Effect.succeed({
5972 status: "started" as const,5955 status: "started" as const,
5973 scriptId: "setup",5956 scriptId: "setup",
...@@ -6101,7 +6084,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -6101,7 +6084,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
6101 Effect.gen(function* () {6084 Effect.gen(function* () {
6102 const dispatchedCommands: Array<OrchestrationCommand> = [];6085 const dispatchedCommands: Array<OrchestrationCommand> = [];
6103 const createWorktree = vi.fn(6086 const createWorktree = vi.fn(
6104 (_: Parameters<GitVcsDriver.GitVcsDriverShape["createWorktree"]>[0]) =>6087 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["createWorktree"]>[0]) =>
6105 Effect.succeed({6088 Effect.succeed({
6106 worktree: {6089 worktree: {
6107 refName: "t3code/bootstrap-refName",6090 refName: "t3code/bootstrap-refName",
...@@ -6110,8 +6093,16 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -6110,8 +6093,16 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
6110 }),6093 }),
6111 );6094 );
6112 const runForThread = vi.fn(6095 const runForThread = vi.fn(
6113 (_: Parameters<ProjectSetupScriptRunnerShape["runForThread"]>[0]) =>6096 (
6114 Effect.fail(new ProjectSetupScriptRunnerError({ message: "pty unavailable" })),6097 _: Parameters<
6098 ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"]
6099 >[0],
6100 ) =>
6101 Effect.fail(
6102 new ProjectSetupScriptRunner.ProjectSetupScriptRunnerError({
6103 message: "pty unavailable",
6104 }),
6105 ),
6115 );6106 );
61166107
6117 yield* buildAppUnderTest({6108 yield* buildAppUnderTest({
...@@ -6195,7 +6186,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -6195,7 +6186,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
6195 Effect.gen(function* () {6186 Effect.gen(function* () {
6196 const dispatchedCommands: Array<OrchestrationCommand> = [];6187 const dispatchedCommands: Array<OrchestrationCommand> = [];
6197 const createWorktree = vi.fn(6188 const createWorktree = vi.fn(
6198 (_: Parameters<GitVcsDriver.GitVcsDriverShape["createWorktree"]>[0]) =>6189 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["createWorktree"]>[0]) =>
6199 Effect.succeed({6190 Effect.succeed({
6200 worktree: {6191 worktree: {
6201 refName: "t3code/bootstrap-refName",6192 refName: "t3code/bootstrap-refName",
...@@ -6204,7 +6195,11 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -6204,7 +6195,11 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
6204 }),6195 }),
6205 );6196 );
6206 const runForThread = vi.fn(6197 const runForThread = vi.fn(
6207 (_: Parameters<ProjectSetupScriptRunnerShape["runForThread"]>[0]) =>6198 (
6199 _: Parameters<
6200 ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"]
6201 >[0],
6202 ) =>
6208 Effect.succeed({6203 Effect.succeed({
6209 status: "started" as const,6204 status: "started" as const,
6210 scriptId: "setup",6205 scriptId: "setup",
...@@ -6314,7 +6309,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -6314,7 +6309,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
6314 Effect.gen(function* () {6309 Effect.gen(function* () {
6315 const dispatchedCommands: Array<OrchestrationCommand> = [];6310 const dispatchedCommands: Array<OrchestrationCommand> = [];
6316 const createWorktree = vi.fn(6311 const createWorktree = vi.fn(
6317 (_: Parameters<GitVcsDriver.GitVcsDriverShape["createWorktree"]>[0]) =>6312 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["createWorktree"]>[0]) =>
6318 Effect.die(new Error("worktree exploded")),6313 Effect.die(new Error("worktree exploded")),
6319 );6314 );
63206315
apps/server/src/server.ts+17-17
...@@ -4,7 +4,7 @@ import * as Layer from "effect/Layer";...@@ -4,7 +4,7 @@ import * as Layer from "effect/Layer";
4import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http";4import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http";
5import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";5import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";
66
7import { ServerConfig } from "./config.ts";7import * as ServerConfig from "./config.ts";
8import {8import {
9 otlpTracesProxyRouteLayer,9 otlpTracesProxyRouteLayer,
10 assetRouteLayer,10 assetRouteLayer,
...@@ -16,15 +16,15 @@ import { fixPath } from "./os-jank.ts";...@@ -16,15 +16,15 @@ import { fixPath } from "./os-jank.ts";
16import { websocketRpcRouteLayer } from "./ws.ts";16import { websocketRpcRouteLayer } from "./ws.ts";
17import * as ExternalLauncher from "./process/externalLauncher.ts";17import * as ExternalLauncher from "./process/externalLauncher.ts";
18import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";18import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";
19import { ServerLifecycleEventsLive } from "./serverLifecycleEvents.ts";19import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";
20import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService.ts";20import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService.ts";
21import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory.ts";21import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory.ts";
22import { ProviderSessionRuntimeRepositoryLive } from "./persistence/Layers/ProviderSessionRuntime.ts";22import { ProviderSessionRuntimeRepositoryLive } from "./persistence/Layers/ProviderSessionRuntime.ts";
23import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry.ts";23import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry.ts";
24import { ProviderEventLoggersLive } from "./provider/Layers/ProviderEventLoggers.ts";24import * as ProviderEventLoggers from "./provider/Layers/ProviderEventLoggers.ts";
25import { ProviderServiceLive } from "./provider/Layers/ProviderService.ts";25import { ProviderServiceLive } from "./provider/Layers/ProviderService.ts";
26import { ProviderSessionReaperLive } from "./provider/Layers/ProviderSessionReaper.ts";26import { ProviderSessionReaperLive } from "./provider/Layers/ProviderSessionReaper.ts";
27import { OpenCodeRuntimeLive } from "./provider/opencodeRuntime.ts";27import * as OpenCodeRuntime from "./provider/opencodeRuntime.ts";
28import { CheckpointDiffQueryLive } from "./checkpointing/Layers/CheckpointDiffQuery.ts";28import { CheckpointDiffQueryLive } from "./checkpointing/Layers/CheckpointDiffQuery.ts";
29import { CheckpointStoreLive } from "./checkpointing/Layers/CheckpointStore.ts";29import { CheckpointStoreLive } from "./checkpointing/Layers/CheckpointStore.ts";
30import * as AzureDevOpsCli from "./sourceControl/AzureDevOpsCli.ts";30import * as AzureDevOpsCli from "./sourceControl/AzureDevOpsCli.ts";
...@@ -40,8 +40,8 @@ import * as PreviewManager from "./preview/Manager.ts";...@@ -40,8 +40,8 @@ import * as PreviewManager from "./preview/Manager.ts";
40import * as PortScanner from "./preview/PortScanner.ts";40import * as PortScanner from "./preview/PortScanner.ts";
41import * as ProcessRunner from "./processRunner.ts";41import * as ProcessRunner from "./processRunner.ts";
42import * as GitManager from "./git/GitManager.ts";42import * as GitManager from "./git/GitManager.ts";
43import { KeybindingsLive } from "./keybindings.ts";43import * as Keybindings from "./keybindings.ts";
44import { ServerRuntimeStartup, ServerRuntimeStartupLive } from "./serverRuntimeStartup.ts";44import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts";
45import { OrchestrationReactorLive } from "./orchestration/Layers/OrchestrationReactor.ts";45import { OrchestrationReactorLive } from "./orchestration/Layers/OrchestrationReactor.ts";
46import { RuntimeReceiptBusLive } from "./orchestration/Layers/RuntimeReceiptBus.ts";46import { RuntimeReceiptBusLive } from "./orchestration/Layers/RuntimeReceiptBus.ts";
47import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion.ts";47import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion.ts";
...@@ -51,7 +51,7 @@ import { ThreadDeletionReactorLive } from "./orchestration/Layers/ThreadDeletion...@@ -51,7 +51,7 @@ import { ThreadDeletionReactorLive } from "./orchestration/Layers/ThreadDeletion
51import * as AgentAwarenessRelay from "./relay/AgentAwarenessRelay.ts";51import * as AgentAwarenessRelay from "./relay/AgentAwarenessRelay.ts";
52import { hasCloudPublicConfig } from "./cloud/publicConfig.ts";52import { hasCloudPublicConfig } from "./cloud/publicConfig.ts";
53import { ProviderRegistryLive } from "./provider/Layers/ProviderRegistry.ts";53import { ProviderRegistryLive } from "./provider/Layers/ProviderRegistry.ts";
54import { ServerSettingsLive } from "./serverSettings.ts";54import * as ServerSettings from "./serverSettings.ts";
55import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts";55import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts";
56import { RepositoryIdentityResolverLive } from "./project/Layers/RepositoryIdentityResolver.ts";56import { RepositoryIdentityResolverLive } from "./project/Layers/RepositoryIdentityResolver.ts";
57import * as WorkspaceEntries from "./workspace/WorkspaceEntries.ts";57import * as WorkspaceEntries from "./workspace/WorkspaceEntries.ts";
...@@ -112,14 +112,14 @@ const PtyAdapterLive = Layer.unwrap(...@@ -112,14 +112,14 @@ const PtyAdapterLive = Layer.unwrap(
112112
113const RelayClientLive = Layer.unwrap(113const RelayClientLive = Layer.unwrap(
114 Effect.gen(function* () {114 Effect.gen(function* () {
115 const config = yield* ServerConfig;115 const config = yield* ServerConfig.ServerConfig;
116 return RelayClient.layerCloudflared({ baseDir: config.baseDir });116 return RelayClient.layerCloudflared({ baseDir: config.baseDir });
117 }),117 }),
118);118);
119119
120const HttpServerLive = Layer.unwrap(120const HttpServerLive = Layer.unwrap(
121 Effect.gen(function* () {121 Effect.gen(function* () {
122 const config = yield* ServerConfig;122 const config = yield* ServerConfig.ServerConfig;
123 if (typeof Bun !== "undefined") {123 if (typeof Bun !== "undefined") {
124 const BunHttpServer = yield* Effect.promise(124 const BunHttpServer = yield* Effect.promise(
125 () => import("@effect/platform-bun/BunHttpServer"),125 () => import("@effect/platform-bun/BunHttpServer"),
...@@ -292,7 +292,7 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(...@@ -292,7 +292,7 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(
292 Layer.provideMerge(ProviderRuntimeLayerLive),292 Layer.provideMerge(ProviderRuntimeLayerLive),
293 Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive)),293 Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive)),
294 Layer.provideMerge(PersistenceLayerLive),294 Layer.provideMerge(PersistenceLayerLive),
295 Layer.provideMerge(KeybindingsLive),295 Layer.provideMerge(Keybindings.layer),
296 Layer.provideMerge(ProviderRegistryLive),296 Layer.provideMerge(ProviderRegistryLive),
297 // The instance registry is the new routing keystone — text generation,297 // The instance registry is the new routing keystone — text generation,
298 // adapter lookup, and runtime ingestion all resolve `ProviderInstanceId`298 // adapter lookup, and runtime ingestion all resolve `ProviderInstanceId`
...@@ -305,14 +305,14 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(...@@ -305,14 +305,14 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(
305 // `ProviderService` (canonical stream, written after event normalization).305 // `ProviderService` (canonical stream, written after event normalization).
306 // Provided once at the runtime level so every consumer sees the same306 // Provided once at the runtime level so every consumer sees the same
307 // logger instances.307 // logger instances.
308 Layer.provideMerge(ProviderEventLoggersLive),308 Layer.provideMerge(ProviderEventLoggers.ProviderEventLoggersLive),
309 // `OpenCodeDriver.create()` yields `OpenCodeRuntime`; previously the old309 // `OpenCodeDriver.create()` yields `OpenCodeRuntime`; previously the old
310 // `ProviderRegistryLive` pulled `OpenCodeRuntimeLive` in for itself, but310 // `ProviderRegistryLive` pulled `OpenCodeRuntimeLive` in for itself, but
311 // the rewritten registry reads snapshots off the instance registry and311 // the rewritten registry reads snapshots off the instance registry and
312 // no longer transitively provides it. Exposing it at the runtime level312 // no longer transitively provides it. Exposing it at the runtime level
313 // keeps a single Live for all opencode consumers.313 // keeps a single Live for all opencode consumers.
314 Layer.provideMerge(OpenCodeRuntimeLive),314 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
315 Layer.provideMerge(ServerSettingsLive),315 Layer.provideMerge(ServerSettings.layer.pipe(Layer.provide(ServerSecretStore.layer))),
316 Layer.provideMerge(WorkspaceLayerLive),316 Layer.provideMerge(WorkspaceLayerLive),
317 Layer.provideMerge(ProjectFaviconResolverLayerLive),317 Layer.provideMerge(ProjectFaviconResolverLayerLive),
318 Layer.provideMerge(RepositoryIdentityResolverLive),318 Layer.provideMerge(RepositoryIdentityResolverLive),
...@@ -334,11 +334,11 @@ const RuntimeDependenciesLive = RuntimeCoreDependenciesLive.pipe(...@@ -334,11 +334,11 @@ const RuntimeDependenciesLive = RuntimeCoreDependenciesLive.pipe(
334 Layer.provideMerge(TraceDiagnostics.layer),334 Layer.provideMerge(TraceDiagnostics.layer),
335 Layer.provideMerge(AnalyticsServiceLayerLive),335 Layer.provideMerge(AnalyticsServiceLayerLive),
336 Layer.provideMerge(ExternalLauncher.layer),336 Layer.provideMerge(ExternalLauncher.layer),
337 Layer.provideMerge(ServerLifecycleEventsLive),337 Layer.provideMerge(ServerLifecycleEvents.layer),
338 Layer.provide(NetService.layer),338 Layer.provide(NetService.layer),
339);339);
340340
341const RuntimeServicesLive = ServerRuntimeStartupLive.pipe(341const RuntimeServicesLive = ServerRuntimeStartup.layer.pipe(
342 Layer.provideMerge(RuntimeDependenciesLive),342 Layer.provideMerge(RuntimeDependenciesLive),
343);343);
344344
...@@ -361,14 +361,14 @@ export const makeRoutesLayer = Layer.mergeAll(...@@ -361,14 +361,14 @@ export const makeRoutesLayer = Layer.mergeAll(
361361
362export const makeServerLayer = Layer.unwrap(362export const makeServerLayer = Layer.unwrap(
363 Effect.gen(function* () {363 Effect.gen(function* () {
364 const config = yield* ServerConfig;364 const config = yield* ServerConfig.ServerConfig;
365365
366 yield* fixPath();366 yield* fixPath();
367367
368 const httpListeningLayer = Layer.effectDiscard(368 const httpListeningLayer = Layer.effectDiscard(
369 Effect.gen(function* () {369 Effect.gen(function* () {
370 yield* HttpServer.HttpServer;370 yield* HttpServer.HttpServer;
371 const startup = yield* ServerRuntimeStartup;371 const startup = yield* ServerRuntimeStartup.ServerRuntimeStartup;
372 yield* startup.markHttpListening;372 yield* startup.markHttpListening;
373 }),373 }),
374 );374 );
apps/server/src/serverLifecycleEvents.test.ts+3-3
...@@ -4,13 +4,13 @@ import { assertTrue } from "@effect/vitest/utils";...@@ -4,13 +4,13 @@ import { assertTrue } from "@effect/vitest/utils";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as Option from "effect/Option";5import * as Option from "effect/Option";
66
7import { ServerLifecycleEvents, ServerLifecycleEventsLive } from "./serverLifecycleEvents.ts";7import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";
88
9it.effect(9it.effect(
10 "publishes lifecycle events without subscribers and snapshots the latest welcome/ready",10 "publishes lifecycle events without subscribers and snapshots the latest welcome/ready",
11 () =>11 () =>
12 Effect.gen(function* () {12 Effect.gen(function* () {
13 const lifecycleEvents = yield* ServerLifecycleEvents;13 const lifecycleEvents = yield* ServerLifecycleEvents.ServerLifecycleEvents;
14 const environment = {14 const environment = {
15 environmentId: EnvironmentId.make("environment-test"),15 environmentId: EnvironmentId.make("environment-test"),
16 label: "Test environment",16 label: "Test environment",
...@@ -49,5 +49,5 @@ it.effect(...@@ -49,5 +49,5 @@ it.effect(
49 const snapshot = yield* lifecycleEvents.snapshot;49 const snapshot = yield* lifecycleEvents.snapshot;
50 assert.equal(snapshot.sequence, 2);50 assert.equal(snapshot.sequence, 2);
51 assert.deepEqual(snapshot.events.map((event) => event.type).toSorted(), ["ready", "welcome"]);51 assert.deepEqual(snapshot.events.map((event) => event.type).toSorted(), ["ready", "welcome"]);
52 }).pipe(Effect.provide(ServerLifecycleEventsLive)),52 }).pipe(Effect.provide(ServerLifecycleEvents.layer)),
53);53);
apps/server/src/serverLifecycleEvents.ts+34-37
...@@ -1,9 +1,9 @@...@@ -1,9 +1,9 @@
1import type { ServerLifecycleStreamEvent } from "@t3tools/contracts";1import type { ServerLifecycleStreamEvent } from "@t3tools/contracts";
2import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
4import * as PubSub from "effect/PubSub";5import * as PubSub from "effect/PubSub";
5import * as Ref from "effect/Ref";6import * as Ref from "effect/Ref";
6import * as Context from "effect/Context";
7import * as Stream from "effect/Stream";7import * as Stream from "effect/Stream";
88
9type LifecycleEventInput =9type LifecycleEventInput =
...@@ -15,44 +15,41 @@ interface SnapshotState {...@@ -15,44 +15,41 @@ interface SnapshotState {
15 readonly events: ReadonlyArray<ServerLifecycleStreamEvent>;15 readonly events: ReadonlyArray<ServerLifecycleStreamEvent>;
16}16}
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
24export class ServerLifecycleEvents extends Context.Service<18export class ServerLifecycleEvents extends Context.Service<
25 ServerLifecycleEvents,19 ServerLifecycleEvents,
26 ServerLifecycleEventsShape20 {
21 readonly publish: (event: LifecycleEventInput) => Effect.Effect<ServerLifecycleStreamEvent>;
22 readonly snapshot: Effect.Effect<SnapshotState>;
23 readonly stream: Stream.Stream<ServerLifecycleStreamEvent>;
24 }
27>()("t3/serverLifecycleEvents") {}25>()("t3/serverLifecycleEvents") {}
2826
29export const ServerLifecycleEventsLive = Layer.effect(27const make = Effect.gen(function* () {
30 ServerLifecycleEvents,28 const pubsub = yield* PubSub.unbounded<ServerLifecycleStreamEvent>();
31 Effect.gen(function* () {29 const state = yield* Ref.make<SnapshotState>({
32 const pubsub = yield* PubSub.unbounded<ServerLifecycleStreamEvent>();30 sequence: 0,
33 const state = yield* Ref.make<SnapshotState>({31 events: [],
34 sequence: 0,32 });
35 events: [],33
36 });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 {55export const layer = Layer.effect(ServerLifecycleEvents, make);
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);
apps/server/src/serverRuntimeStartup.test.ts+32-42
...@@ -10,24 +10,14 @@ import * as PlatformError from "effect/PlatformError";...@@ -10,24 +10,14 @@ import * as PlatformError from "effect/PlatformError";
10import * as Ref from "effect/Ref";10import * as Ref from "effect/Ref";
11import * as Stream from "effect/Stream";11import * as Stream from "effect/Stream";
1212
13import { ServerConfig } from "./config.ts";13import * as ServerConfig from "./config.ts";
14import {14import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts";
15 OrchestrationEngineService,15import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts";
16 type OrchestrationEngineShape,16import * as AnalyticsService from "./telemetry/Services/AnalyticsService.ts";
17} from "./orchestration/Services/OrchestrationEngine.ts";17import * as ServerRuntimeStartup from "./serverRuntimeStartup.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";
2818
29it("uses the canonical Codex default for auto-bootstrapped model selection", () => {19it("uses the canonical Codex default for auto-bootstrapped model selection", () => {
30 assert.deepStrictEqual(getAutoBootstrapDefaultModelSelection(), {20 assert.deepStrictEqual(ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(), {
31 instanceId: ProviderInstanceId.make("codex"),21 instanceId: ProviderInstanceId.make("codex"),
32 model: DEFAULT_MODEL,22 model: DEFAULT_MODEL,
33 });23 });
...@@ -37,7 +27,7 @@ it.effect("enqueueCommand waits for readiness and then drains queued work", () =...@@ -37,7 +27,7 @@ it.effect("enqueueCommand waits for readiness and then drains queued work", () =
37 Effect.scoped(27 Effect.scoped(
38 Effect.gen(function* () {28 Effect.gen(function* () {
39 const executionCount = yield* Ref.make(0);29 const executionCount = yield* Ref.make(0);
40 const commandGate = yield* makeCommandGate;30 const commandGate = yield* ServerRuntimeStartup.makeCommandGate;
4131
42 const queuedCommandFiber = yield* commandGate32 const queuedCommandFiber = yield* commandGate
43 .enqueueCommand(Ref.updateAndGet(executionCount, (count) => count + 1))33 .enqueueCommand(Ref.updateAndGet(executionCount, (count) => count + 1))
...@@ -58,7 +48,7 @@ it.effect("enqueueCommand waits for readiness and then drains queued work", () =...@@ -58,7 +48,7 @@ it.effect("enqueueCommand waits for readiness and then drains queued work", () =
58it.effect("enqueueCommand fails queued work when readiness fails", () =>48it.effect("enqueueCommand fails queued work when readiness fails", () =>
59 Effect.scoped(49 Effect.scoped(
60 Effect.gen(function* () {50 Effect.gen(function* () {
61 const commandGate = yield* makeCommandGate;51 const commandGate = yield* ServerRuntimeStartup.makeCommandGate;
62 const failure = yield* Deferred.make<void, never>();52 const failure = yield* Deferred.make<void, never>();
6353
64 const queuedCommandFiber = yield* commandGate54 const queuedCommandFiber = yield* commandGate
...@@ -66,13 +56,13 @@ it.effect("enqueueCommand fails queued work when readiness fails", () =>...@@ -66,13 +56,13 @@ it.effect("enqueueCommand fails queued work when readiness fails", () =>
66 .pipe(Effect.forkScoped);56 .pipe(Effect.forkScoped);
6757
68 yield* commandGate.failCommandReady(58 yield* commandGate.failCommandReady(
69 new ServerRuntimeStartupError({59 new ServerRuntimeStartup.ServerRuntimeStartupError({
70 message: "startup failed",60 stage: "command-readiness",
71 }),61 }),
72 );62 );
7363
74 const error = yield* Effect.flip(Fiber.join(queuedCommandFiber));64 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.");
76 }),66 }),
77 ),67 ),
78);68);
...@@ -82,8 +72,8 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa...@@ -82,8 +72,8 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa
82 Effect.gen(function* () {72 Effect.gen(function* () {
83 const releaseCounts = yield* Deferred.make<void, never>();73 const releaseCounts = yield* Deferred.make<void, never>();
8474
85 yield* launchStartupHeartbeat.pipe(75 yield* ServerRuntimeStartup.launchStartupHeartbeat.pipe(
86 Effect.provideService(ProjectionSnapshotQuery, {76 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
87 getCommandReadModel: () => Effect.die("unused"),77 getCommandReadModel: () => Effect.die("unused"),
88 getSnapshot: () => Effect.die("unused"),78 getSnapshot: () => Effect.die("unused"),
89 getShellSnapshot: () => Effect.die("unused"),79 getShellSnapshot: () => Effect.die("unused"),
...@@ -104,7 +94,7 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa...@@ -104,7 +94,7 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa
104 getThreadShellById: () => Effect.succeed(Option.none()),94 getThreadShellById: () => Effect.succeed(Option.none()),
105 getThreadDetailById: () => Effect.succeed(Option.none()),95 getThreadDetailById: () => Effect.succeed(Option.none()),
106 }),96 }),
107 Effect.provideService(AnalyticsService, {97 Effect.provideService(AnalyticsService.AnalyticsService, {
108 record: () => Effect.void,98 record: () => Effect.void,
109 flush: Effect.void,99 flush: Effect.void,
110 }),100 }),
...@@ -115,8 +105,8 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa...@@ -115,8 +105,8 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa
115105
116it.effect("resolveWelcomeBase derives cwd and project name from server config", () =>106it.effect("resolveWelcomeBase derives cwd and project name from server config", () =>
117 Effect.gen(function* () {107 Effect.gen(function* () {
118 const welcome = yield* resolveWelcomeBase.pipe(108 const welcome = yield* ServerRuntimeStartup.resolveWelcomeBase.pipe(
119 Effect.provideService(ServerConfig, {109 Effect.provideService(ServerConfig.ServerConfig, {
120 cwd: "/tmp/startup-project",110 cwd: "/tmp/startup-project",
121 } as never),111 } as never),
122 );112 );
...@@ -134,12 +124,12 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa...@@ -134,12 +124,12 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa
134124
135 return Effect.gen(function* () {125 return Effect.gen(function* () {
136 const dispatchCalls = yield* Ref.make<ReadonlyArray<string>>([]);126 const dispatchCalls = yield* Ref.make<ReadonlyArray<string>>([]);
137 const targets = yield* resolveAutoBootstrapWelcomeTargets.pipe(127 const targets = yield* ServerRuntimeStartup.resolveAutoBootstrapWelcomeTargets.pipe(
138 Effect.provideService(ServerConfig, {128 Effect.provideService(ServerConfig.ServerConfig, {
139 cwd: "/tmp/startup-project",129 cwd: "/tmp/startup-project",
140 autoBootstrapProjectFromCwd: true,130 autoBootstrapProjectFromCwd: true,
141 } as never),131 } as never),
142 Effect.provideService(ProjectionSnapshotQuery, {132 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
143 getCommandReadModel: () => Effect.die("unused"),133 getCommandReadModel: () => Effect.die("unused"),
144 getSnapshot: () => Effect.die("unused"),134 getSnapshot: () => Effect.die("unused"),
145 getShellSnapshot: () => Effect.die("unused"),135 getShellSnapshot: () => Effect.die("unused"),
...@@ -152,7 +142,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa...@@ -152,7 +142,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa
152 id: bootstrapProjectId,142 id: bootstrapProjectId,
153 title: "Startup Project",143 title: "Startup Project",
154 workspaceRoot: "/tmp/startup-project",144 workspaceRoot: "/tmp/startup-project",
155 defaultModelSelection: getAutoBootstrapDefaultModelSelection(),145 defaultModelSelection: ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(),
156 scripts: [],146 scripts: [],
157 createdAt: "2026-01-01T00:00:00.000Z",147 createdAt: "2026-01-01T00:00:00.000Z",
158 updatedAt: "2026-01-01T00:00:00.000Z",148 updatedAt: "2026-01-01T00:00:00.000Z",
...@@ -166,14 +156,14 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa...@@ -166,14 +156,14 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa
166 getThreadShellById: () => Effect.die("unused"),156 getThreadShellById: () => Effect.die("unused"),
167 getThreadDetailById: () => Effect.die("unused"),157 getThreadDetailById: () => Effect.die("unused"),
168 }),158 }),
169 Effect.provideService(OrchestrationEngineService, {159 Effect.provideService(OrchestrationEngine.OrchestrationEngineService, {
170 readEvents: () => Stream.empty,160 readEvents: () => Stream.empty,
171 dispatch: (command) =>161 dispatch: (command) =>
172 Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe(162 Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe(
173 Effect.as({ sequence: 1 }),163 Effect.as({ sequence: 1 }),
174 ),164 ),
175 streamDomainEvents: Stream.empty,165 streamDomainEvents: Stream.empty,
176 } satisfies OrchestrationEngineShape),166 } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]),
177 Effect.provide(NodeServices.layer),167 Effect.provide(NodeServices.layer),
178 );168 );
179169
...@@ -188,12 +178,12 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa...@@ -188,12 +178,12 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa
188it.effect("resolveAutoBootstrapWelcomeTargets creates a project and thread when missing", () =>178it.effect("resolveAutoBootstrapWelcomeTargets creates a project and thread when missing", () =>
189 Effect.gen(function* () {179 Effect.gen(function* () {
190 const dispatchCalls = yield* Ref.make<ReadonlyArray<string>>([]);180 const dispatchCalls = yield* Ref.make<ReadonlyArray<string>>([]);
191 const targets = yield* resolveAutoBootstrapWelcomeTargets.pipe(181 const targets = yield* ServerRuntimeStartup.resolveAutoBootstrapWelcomeTargets.pipe(
192 Effect.provideService(ServerConfig, {182 Effect.provideService(ServerConfig.ServerConfig, {
193 cwd: "/tmp/startup-project",183 cwd: "/tmp/startup-project",
194 autoBootstrapProjectFromCwd: true,184 autoBootstrapProjectFromCwd: true,
195 } as never),185 } as never),
196 Effect.provideService(ProjectionSnapshotQuery, {186 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
197 getCommandReadModel: () => Effect.die("unused"),187 getCommandReadModel: () => Effect.die("unused"),
198 getSnapshot: () => Effect.die("unused"),188 getSnapshot: () => Effect.die("unused"),
199 getShellSnapshot: () => Effect.die("unused"),189 getShellSnapshot: () => Effect.die("unused"),
...@@ -208,14 +198,14 @@ it.effect("resolveAutoBootstrapWelcomeTargets creates a project and thread when...@@ -208,14 +198,14 @@ it.effect("resolveAutoBootstrapWelcomeTargets creates a project and thread when
208 getThreadShellById: () => Effect.die("unused"),198 getThreadShellById: () => Effect.die("unused"),
209 getThreadDetailById: () => Effect.die("unused"),199 getThreadDetailById: () => Effect.die("unused"),
210 }),200 }),
211 Effect.provideService(OrchestrationEngineService, {201 Effect.provideService(OrchestrationEngine.OrchestrationEngineService, {
212 readEvents: () => Stream.empty,202 readEvents: () => Stream.empty,
213 dispatch: (command) =>203 dispatch: (command) =>
214 Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe(204 Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe(
215 Effect.as({ sequence: 1 }),205 Effect.as({ sequence: 1 }),
216 ),206 ),
217 streamDomainEvents: Stream.empty,207 streamDomainEvents: Stream.empty,
218 } satisfies OrchestrationEngineShape),208 } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]),
219 Effect.provide(NodeServices.layer),209 Effect.provide(NodeServices.layer),
220 );210 );
221211
...@@ -236,12 +226,12 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa...@@ -236,12 +226,12 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa
236 });226 });
237 const dispatchCalls = yield* Ref.make<ReadonlyArray<string>>([]);227 const dispatchCalls = yield* Ref.make<ReadonlyArray<string>>([]);
238228
239 const error = yield* resolveAutoBootstrapWelcomeTargets.pipe(229 const error = yield* ServerRuntimeStartup.resolveAutoBootstrapWelcomeTargets.pipe(
240 Effect.provideService(ServerConfig, {230 Effect.provideService(ServerConfig.ServerConfig, {
241 cwd: "/tmp/startup-project",231 cwd: "/tmp/startup-project",
242 autoBootstrapProjectFromCwd: true,232 autoBootstrapProjectFromCwd: true,
243 } as never),233 } as never),
244 Effect.provideService(ProjectionSnapshotQuery, {234 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
245 getCommandReadModel: () => Effect.die("unused"),235 getCommandReadModel: () => Effect.die("unused"),
246 getSnapshot: () => Effect.die("unused"),236 getSnapshot: () => Effect.die("unused"),
247 getShellSnapshot: () => Effect.die("unused"),237 getShellSnapshot: () => Effect.die("unused"),
...@@ -256,14 +246,14 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa...@@ -256,14 +246,14 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa
256 getThreadShellById: () => Effect.die("unused"),246 getThreadShellById: () => Effect.die("unused"),
257 getThreadDetailById: () => Effect.die("unused"),247 getThreadDetailById: () => Effect.die("unused"),
258 }),248 }),
259 Effect.provideService(OrchestrationEngineService, {249 Effect.provideService(OrchestrationEngine.OrchestrationEngineService, {
260 readEvents: () => Stream.empty,250 readEvents: () => Stream.empty,
261 dispatch: (command) =>251 dispatch: (command) =>
262 Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe(252 Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe(
263 Effect.as({ sequence: 1 }),253 Effect.as({ sequence: 1 }),
264 ),254 ),
265 streamDomainEvents: Stream.empty,255 streamDomainEvents: Stream.empty,
266 } satisfies OrchestrationEngineShape),256 } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]),
267 Effect.provideService(Crypto.Crypto, {257 Effect.provideService(Crypto.Crypto, {
268 ...crypto,258 ...crypto,
269 randomUUIDv4: Effect.fail(uuidError),259 randomUUIDv4: Effect.fail(uuidError),
apps/server/src/serverRuntimeStartup.ts+53-48
...@@ -7,8 +7,10 @@ import {...@@ -7,8 +7,10 @@ import {
7 ProviderInstanceId,7 ProviderInstanceId,
8 ThreadId,8 ThreadId,
9} from "@t3tools/contracts";9} from "@t3tools/contracts";
10import * as Data from "effect/Data";10import * as Console from "effect/Console";
11import * as Context from "effect/Context";
11import * as Crypto from "effect/Crypto";12import * as Crypto from "effect/Crypto";
13import * as DateTime from "effect/DateTime";
12import * as Deferred from "effect/Deferred";14import * as Deferred from "effect/Deferred";
13import * as Effect from "effect/Effect";15import * as Effect from "effect/Effect";
14import * as Exit from "effect/Exit";16import * as Exit from "effect/Exit";
...@@ -17,23 +19,21 @@ import * as Option from "effect/Option";...@@ -17,23 +19,21 @@ import * as Option from "effect/Option";
17import * as Path from "effect/Path";19import * as Path from "effect/Path";
18import * as Queue from "effect/Queue";20import * as Queue from "effect/Queue";
19import * as Ref from "effect/Ref";21import * as Ref from "effect/Ref";
22import * as Schema from "effect/Schema";
20import * as Scope from "effect/Scope";23import * 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";25import * as ServerConfig from "./config.ts";
26import { Keybindings } from "./keybindings.ts";26import * as Keybindings from "./keybindings.ts";
27import * as ExternalLauncher from "./process/externalLauncher.ts";27import * as ExternalLauncher from "./process/externalLauncher.ts";
28import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine.ts";28import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts";
29import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts";29import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts";
30import { OrchestrationReactor } from "./orchestration/Services/OrchestrationReactor.ts";30import * as OrchestrationReactor from "./orchestration/Services/OrchestrationReactor.ts";
31import { ServerLifecycleEvents } from "./serverLifecycleEvents.ts";31import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";
32import { ServerSettingsService } from "./serverSettings.ts";32import * as ServerSettings from "./serverSettings.ts";
33import { ServerEnvironment } from "./environment/Services/ServerEnvironment.ts";33import * as ServerEnvironment from "./environment/Services/ServerEnvironment.ts";
34import { AnalyticsService } from "./telemetry/Services/AnalyticsService.ts";34import * as AnalyticsService from "./telemetry/Services/AnalyticsService.ts";
35import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";35import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
36import { ProviderSessionReaper } from "./provider/Services/ProviderSessionReaper.ts";36import * as ProviderSessionReaper from "./provider/Services/ProviderSessionReaper.ts";
37import {37import {
38 formatHeadlessServeOutput,38 formatHeadlessServeOutput,
39 formatHostForUrl,39 formatHostForUrl,
...@@ -41,22 +41,30 @@ import {...@@ -41,22 +41,30 @@ import {
41 issueHeadlessServeAccessInfo,41 issueHeadlessServeAccessInfo,
42} from "./startupAccess.ts";42} from "./startupAccess.ts";
4343
44export class ServerRuntimeStartupError extends Data.TaggedError("ServerRuntimeStartupError")<{44export class ServerRuntimeStartupError extends Schema.TaggedErrorClass<ServerRuntimeStartupError>()(
45 readonly message: string;45 "ServerRuntimeStartupError",
46 readonly cause?: unknown;46 {
47}> {}47 stage: Schema.Literal("command-readiness"),
4848 cause: Schema.optional(Schema.Defect()),
49export interface ServerRuntimeStartupShape {49 },
50 readonly awaitCommandReady: Effect.Effect<void, ServerRuntimeStartupError>;50) {
51 readonly markHttpListening: Effect.Effect<void>;51 override get message(): string {
52 readonly enqueueCommand: <A, E>(52 switch (this.stage) {
53 effect: Effect.Effect<A, E>,53 case "command-readiness":
54 ) => Effect.Effect<A, E | ServerRuntimeStartupError>;54 return "Server runtime startup failed before command readiness.";
55 }
56 }
55}57}
5658
57export class ServerRuntimeStartup extends Context.Service<59export class ServerRuntimeStartup extends Context.Service<
58 ServerRuntimeStartup,60 ServerRuntimeStartup,
59 ServerRuntimeStartupShape61 {
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 }
60>()("t3/serverRuntimeStartup") {}68>()("t3/serverRuntimeStartup") {}
6169
62interface QueuedCommand {70interface QueuedCommand {
...@@ -124,8 +132,8 @@ export const makeCommandGate = Effect.gen(function* () {...@@ -124,8 +132,8 @@ export const makeCommandGate = Effect.gen(function* () {
124});132});
125133
126export const recordStartupHeartbeat = Effect.gen(function* () {134export const recordStartupHeartbeat = Effect.gen(function* () {
127 const analytics = yield* AnalyticsService;135 const analytics = yield* AnalyticsService.AnalyticsService;
128 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;136 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
129137
130 const { threadCount, projectCount } = yield* projectionSnapshotQuery.getCounts().pipe(138 const { threadCount, projectCount } = yield* projectionSnapshotQuery.getCounts().pipe(
131 Effect.catch((cause) =>139 Effect.catch((cause) =>
...@@ -160,7 +168,7 @@ export const getAutoBootstrapDefaultModelSelection = (): ModelSelection => ({...@@ -160,7 +168,7 @@ export const getAutoBootstrapDefaultModelSelection = (): ModelSelection => ({
160});168});
161169
162export const resolveWelcomeBase = Effect.gen(function* () {170export const resolveWelcomeBase = Effect.gen(function* () {
163 const serverConfig = yield* ServerConfig;171 const serverConfig = yield* ServerConfig.ServerConfig;
164 const segments = serverConfig.cwd.split(/[/\\]/).filter(Boolean);172 const segments = serverConfig.cwd.split(/[/\\]/).filter(Boolean);
165 const projectName = segments[segments.length - 1] ?? "project";173 const projectName = segments[segments.length - 1] ?? "project";
166174
...@@ -173,9 +181,9 @@ export const resolveWelcomeBase = Effect.gen(function* () {...@@ -173,9 +181,9 @@ export const resolveWelcomeBase = Effect.gen(function* () {
173export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () {181export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () {
174 const crypto = yield* Crypto.Crypto;182 const crypto = yield* Crypto.Crypto;
175 const randomUUID = crypto.randomUUIDv4;183 const randomUUID = crypto.randomUUIDv4;
176 const serverConfig = yield* ServerConfig;184 const serverConfig = yield* ServerConfig.ServerConfig;
177 const projectionReadModelQuery = yield* ProjectionSnapshotQuery;185 const projectionReadModelQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
178 const orchestrationEngine = yield* OrchestrationEngineService;186 const orchestrationEngine = yield* OrchestrationEngine.OrchestrationEngineService;
179 const path = yield* Path.Path;187 const path = yield* Path.Path;
180188
181 let bootstrapProjectId: ProjectId | undefined;189 let bootstrapProjectId: ProjectId | undefined;
...@@ -243,7 +251,7 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () {...@@ -243,7 +251,7 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () {
243});251});
244252
245const resolveStartupBrowserTarget = Effect.gen(function* () {253const resolveStartupBrowserTarget = Effect.gen(function* () {
246 const serverConfig = yield* ServerConfig;254 const serverConfig = yield* ServerConfig.ServerConfig;
247 const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;255 const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;
248 const localUrl = `http://localhost:${serverConfig.port}`;256 const localUrl = `http://localhost:${serverConfig.port}`;
249 const bindUrl =257 const bindUrl =
...@@ -260,7 +268,7 @@ const resolveStartupBrowserTarget = Effect.gen(function* () {...@@ -260,7 +268,7 @@ const resolveStartupBrowserTarget = Effect.gen(function* () {
260268
261const maybeOpenBrowser = (target: string) =>269const maybeOpenBrowser = (target: string) =>
262 Effect.gen(function* () {270 Effect.gen(function* () {
263 const serverConfig = yield* ServerConfig;271 const serverConfig = yield* ServerConfig.ServerConfig;
264 if (serverConfig.noBrowser) {272 if (serverConfig.noBrowser) {
265 return;273 return;
266 }274 }
...@@ -281,14 +289,14 @@ const runStartupPhase = <A, E, R>(phase: string, effect: Effect.Effect<A, E, R>)...@@ -281,14 +289,14 @@ const runStartupPhase = <A, E, R>(phase: string, effect: Effect.Effect<A, E, R>)
281 Effect.withSpan(`server.startup.${phase}`),289 Effect.withSpan(`server.startup.${phase}`),
282 );290 );
283291
284export const makeServerRuntimeStartup = Effect.gen(function* () {292const make = Effect.gen(function* () {
285 const serverConfig = yield* ServerConfig;293 const serverConfig = yield* ServerConfig.ServerConfig;
286 const keybindings = yield* Keybindings;294 const keybindings = yield* Keybindings.Keybindings;
287 const orchestrationReactor = yield* OrchestrationReactor;295 const orchestrationReactor = yield* OrchestrationReactor.OrchestrationReactor;
288 const providerSessionReaper = yield* ProviderSessionReaper;296 const providerSessionReaper = yield* ProviderSessionReaper.ProviderSessionReaper;
289 const lifecycleEvents = yield* ServerLifecycleEvents;297 const lifecycleEvents = yield* ServerLifecycleEvents.ServerLifecycleEvents;
290 const serverSettings = yield* ServerSettingsService;298 const serverSettings = yield* ServerSettings.ServerSettingsService;
291 const serverEnvironment = yield* ServerEnvironment;299 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;
292 const crypto = yield* Crypto.Crypto;300 const crypto = yield* Crypto.Crypto;
293301
294 const commandGate = yield* makeCommandGate;302 const commandGate = yield* makeCommandGate;
...@@ -409,7 +417,7 @@ export const makeServerRuntimeStartup = Effect.gen(function* () {...@@ -409,7 +417,7 @@ export const makeServerRuntimeStartup = Effect.gen(function* () {
409 const startupExit = yield* Effect.exit(startup);417 const startupExit = yield* Effect.exit(startup);
410 if (Exit.isFailure(startupExit)) {418 if (Exit.isFailure(startupExit)) {
411 const error = new ServerRuntimeStartupError({419 const error = new ServerRuntimeStartupError({
412 message: "Server runtime startup failed before command readiness.",420 stage: "command-readiness",
413 cause: startupExit.cause,421 cause: startupExit.cause,
414 });422 });
415 yield* Effect.logError("server runtime startup failed", { cause: startupExit.cause });423 yield* Effect.logError("server runtime startup failed", { cause: startupExit.cause });
...@@ -461,10 +469,7 @@ export const makeServerRuntimeStartup = Effect.gen(function* () {...@@ -461,10 +469,7 @@ export const makeServerRuntimeStartup = Effect.gen(function* () {
461 awaitCommandReady: commandGate.awaitCommandReady,469 awaitCommandReady: commandGate.awaitCommandReady,
462 markHttpListening: Deferred.succeed(httpListening, undefined),470 markHttpListening: Deferred.succeed(httpListening, undefined),
463 enqueueCommand: commandGate.enqueueCommand,471 enqueueCommand: commandGate.enqueueCommand,
464 } satisfies ServerRuntimeStartupShape;472 } satisfies ServerRuntimeStartup["Service"];
465});473});
466474
467export const ServerRuntimeStartupLive = Layer.effect(475export const layer = Layer.effect(ServerRuntimeStartup, make);
468 ServerRuntimeStartup,
469 makeServerRuntimeStartup,
470);
apps/server/src/serverRuntimeState.ts+3-3
...@@ -5,7 +5,7 @@ import * as Option from "effect/Option";...@@ -5,7 +5,7 @@ import * as Option from "effect/Option";
5import * as Schema from "effect/Schema";5import * as Schema from "effect/Schema";
66
7import { writeFileStringAtomically } from "./atomicWrite.ts";7import { writeFileStringAtomically } from "./atomicWrite.ts";
8import { type ServerConfigShape } from "./config.ts";8import type * as ServerConfig from "./config.ts";
9import { formatHostForUrl, isWildcardHost } from "./startupAccess.ts";9import { formatHostForUrl, isWildcardHost } from "./startupAccess.ts";
1010
11export const PersistedServerRuntimeState = Schema.Struct({11export const PersistedServerRuntimeState = Schema.Struct({
...@@ -23,7 +23,7 @@ const decodePersistedServerRuntimeState = Schema.decodeUnknownEffect(...@@ -23,7 +23,7 @@ const decodePersistedServerRuntimeState = Schema.decodeUnknownEffect(
23);23);
2424
25const runtimeOriginForConfig = (25const runtimeOriginForConfig = (
26 config: Pick<ServerConfigShape, "host">,26 config: Pick<ServerConfig.ServerConfig["Service"], "host">,
27 port: number,27 port: number,
28): PersistedServerRuntimeState["origin"] => {28): PersistedServerRuntimeState["origin"] => {
29 const hostname =29 const hostname =
...@@ -32,7 +32,7 @@ const runtimeOriginForConfig = (...@@ -32,7 +32,7 @@ const runtimeOriginForConfig = (
32};32};
3333
34export const makePersistedServerRuntimeState = (input: {34export const makePersistedServerRuntimeState = (input: {
35 readonly config: Pick<ServerConfigShape, "host">;35 readonly config: Pick<ServerConfig.ServerConfig["Service"], "host">;
36 readonly port: number;36 readonly port: number;
37}): Effect.Effect<PersistedServerRuntimeState> =>37}): Effect.Effect<PersistedServerRuntimeState> =>
38 Effect.map(DateTime.now, (now) => ({38 Effect.map(DateTime.now, (now) => ({
apps/server/src/serverSettings.test.ts+19-17
...@@ -13,14 +13,16 @@ import * as Duration from "effect/Duration";...@@ -13,14 +13,16 @@ import * as Duration from "effect/Duration";
13import * as FileSystem from "effect/FileSystem";13import * as FileSystem from "effect/FileSystem";
14import * as Layer from "effect/Layer";14import * as Layer from "effect/Layer";
15import * as Schema from "effect/Schema";15import * as Schema from "effect/Schema";
16import { ServerConfig } from "./config.ts";16import * as ServerSecretStore from "./auth/ServerSecretStore.ts";
17import { ServerSettingsLive, ServerSettingsService } from "./serverSettings.ts";17import * as ServerConfig from "./config.ts";
18import * as ServerSettingsModule from "./serverSettings.ts";
1819
19const decodeSettingsPatch = Schema.decodeUnknownEffect(ServerSettingsPatch);20const decodeSettingsPatch = Schema.decodeUnknownEffect(ServerSettingsPatch);
20const decodeServerSettings = Schema.decodeUnknownEffect(ServerSettings);21const decodeServerSettings = Schema.decodeUnknownEffect(ServerSettings);
2122
22const makeServerSettingsLayer = () =>23const makeServerSettingsLayer = () =>
23 ServerSettingsLive.pipe(24 ServerSettingsModule.layer.pipe(
25 Layer.provide(ServerSecretStore.layer),
24 Layer.provideMerge(26 Layer.provideMerge(
25 Layer.fresh(27 Layer.fresh(
26 ServerConfig.layerTest(process.cwd(), {28 ServerConfig.layerTest(process.cwd(), {
...@@ -77,7 +79,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -77,7 +79,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
7779
78 it.effect("deep merges nested settings updates without dropping siblings", () =>80 it.effect("deep merges nested settings updates without dropping siblings", () =>
79 Effect.gen(function* () {81 Effect.gen(function* () {
80 const serverSettings = yield* ServerSettingsService;82 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
8183
82 yield* serverSettings.updateSettings({84 yield* serverSettings.updateSettings({
83 providers: {85 providers: {
...@@ -145,7 +147,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -145,7 +147,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
145147
146 it.effect("preserves model when switching providers via textGenerationModelSelection", () =>148 it.effect("preserves model when switching providers via textGenerationModelSelection", () =>
147 Effect.gen(function* () {149 Effect.gen(function* () {
148 const serverSettings = yield* ServerSettingsService;150 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
149151
150 // Start with Claude text generation selection152 // Start with Claude text generation selection
151 yield* serverSettings.updateSettings({153 yield* serverSettings.updateSettings({
...@@ -183,7 +185,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -183,7 +185,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
183185
184 it.effect("preserves custom provider instance text generation selections", () =>186 it.effect("preserves custom provider instance text generation selections", () =>
185 Effect.gen(function* () {187 Effect.gen(function* () {
186 const serverSettings = yield* ServerSettingsService;188 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
187189
188 const next = yield* serverSettings.updateSettings({190 const next = yield* serverSettings.updateSettings({
189 providerInstances: {191 providerInstances: {
...@@ -210,7 +212,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -210,7 +212,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
210 "uses explicit provider instance enabled state over legacy provider enabled state",212 "uses explicit provider instance enabled state over legacy provider enabled state",
211 () =>213 () =>
212 Effect.gen(function* () {214 Effect.gen(function* () {
213 const serverSettings = yield* ServerSettingsService;215 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
214 const instanceId = ProviderInstanceId.make("claude_openrouter");216 const instanceId = ProviderInstanceId.make("claude_openrouter");
215217
216 const next = yield* serverSettings.updateSettings({218 const next = yield* serverSettings.updateSettings({
...@@ -241,7 +243,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -241,7 +243,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
241243
242 it.effect("preserves enabled text generation selections for non-built-in drivers", () =>244 it.effect("preserves enabled text generation selections for non-built-in drivers", () =>
243 Effect.gen(function* () {245 Effect.gen(function* () {
244 const serverSettings = yield* ServerSettingsService;246 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
245 const instanceId = ProviderInstanceId.make("openrouter_text");247 const instanceId = ProviderInstanceId.make("openrouter_text");
246248
247 const next = yield* serverSettings.updateSettings({249 const next = yield* serverSettings.updateSettings({
...@@ -267,7 +269,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -267,7 +269,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
267269
268 it.effect("drops stale text generation options when resetting model selection", () =>270 it.effect("drops stale text generation options when resetting model selection", () =>
269 Effect.gen(function* () {271 Effect.gen(function* () {
270 const serverSettings = yield* ServerSettingsService;272 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
271273
272 yield* serverSettings.updateSettings({274 yield* serverSettings.updateSettings({
273 textGenerationModelSelection: {275 textGenerationModelSelection: {
...@@ -300,7 +302,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -300,7 +302,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
300302
301 it.effect("replaces provider instance maps when clearing optional fields", () =>303 it.effect("replaces provider instance maps when clearing optional fields", () =>
302 Effect.gen(function* () {304 Effect.gen(function* () {
303 const serverSettings = yield* ServerSettingsService;305 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
304 const codexId = ProviderInstanceId.make("codex");306 const codexId = ProviderInstanceId.make("codex");
305307
306 yield* serverSettings.updateSettings({308 yield* serverSettings.updateSettings({
...@@ -337,7 +339,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -337,7 +339,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
337339
338 it.effect("trims provider path settings when updates are applied", () =>340 it.effect("trims provider path settings when updates are applied", () =>
339 Effect.gen(function* () {341 Effect.gen(function* () {
340 const serverSettings = yield* ServerSettingsService;342 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
341343
342 const next = yield* serverSettings.updateSettings({344 const next = yield* serverSettings.updateSettings({
343 providers: {345 providers: {
...@@ -382,7 +384,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -382,7 +384,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
382384
383 it.effect("trims observability settings when updates are applied", () =>385 it.effect("trims observability settings when updates are applied", () =>
384 Effect.gen(function* () {386 Effect.gen(function* () {
385 const serverSettings = yield* ServerSettingsService;387 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
386388
387 const next = yield* serverSettings.updateSettings({389 const next = yield* serverSettings.updateSettings({
388 addProjectBaseDirectory: " ~/Development ",390 addProjectBaseDirectory: " ~/Development ",
...@@ -402,7 +404,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -402,7 +404,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
402404
403 it.effect("defaults blank binary paths to provider executables", () =>405 it.effect("defaults blank binary paths to provider executables", () =>
404 Effect.gen(function* () {406 Effect.gen(function* () {
405 const serverSettings = yield* ServerSettingsService;407 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
406408
407 const next = yield* serverSettings.updateSettings({409 const next = yield* serverSettings.updateSettings({
408 providers: {410 providers: {
...@@ -422,8 +424,8 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -422,8 +424,8 @@ it.layer(NodeServices.layer)("server settings", (it) => {
422424
423 it.effect("writes only non-default server settings to disk", () =>425 it.effect("writes only non-default server settings to disk", () =>
424 Effect.gen(function* () {426 Effect.gen(function* () {
425 const serverSettings = yield* ServerSettingsService;427 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
426 const serverConfig = yield* ServerConfig;428 const serverConfig = yield* ServerConfig.ServerConfig;
427 const fileSystem = yield* FileSystem.FileSystem;429 const fileSystem = yield* FileSystem.FileSystem;
428 const next = yield* serverSettings.updateSettings({430 const next = yield* serverSettings.updateSettings({
429 addProjectBaseDirectory: "~/Development",431 addProjectBaseDirectory: "~/Development",
...@@ -469,8 +471,8 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -469,8 +471,8 @@ it.layer(NodeServices.layer)("server settings", (it) => {
469471
470 it.effect("stores sensitive provider instance environment values outside settings.json", () =>472 it.effect("stores sensitive provider instance environment values outside settings.json", () =>
471 Effect.gen(function* () {473 Effect.gen(function* () {
472 const serverSettings = yield* ServerSettingsService;474 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
473 const serverConfig = yield* ServerConfig;475 const serverConfig = yield* ServerConfig.ServerConfig;
474 const fileSystem = yield* FileSystem.FileSystem;476 const fileSystem = yield* FileSystem.FileSystem;
475 const instanceId = ProviderInstanceId.make("codex_personal");477 const instanceId = ProviderInstanceId.make("codex_personal");
476478
apps/server/src/serverSettings.ts+56-57
...@@ -26,26 +26,26 @@ import {...@@ -26,26 +26,26 @@ import {
26 type ServerSettingsPatch,26 type ServerSettingsPatch,
27} from "@t3tools/contracts";27} from "@t3tools/contracts";
28import * as Cache from "effect/Cache";28import * as Cache from "effect/Cache";
29import * as Cause from "effect/Cause";
30import * as Context from "effect/Context";
29import * as Deferred from "effect/Deferred";31import * as Deferred from "effect/Deferred";
30import * as Duration from "effect/Duration";32import * as Duration from "effect/Duration";
33import * as Equal from "effect/Equal";
31import * as Effect from "effect/Effect";34import * as Effect from "effect/Effect";
32import * as Exit from "effect/Exit";35import * as Exit from "effect/Exit";
33import * as FileSystem from "effect/FileSystem";36import * as FileSystem from "effect/FileSystem";
34import * as Layer from "effect/Layer";37import * as Layer from "effect/Layer";
35import * as Option from "effect/Option";38import * as Option from "effect/Option";
36import * as Path from "effect/Path";39import * as Path from "effect/Path";
37import * as Equal from "effect/Equal";
38import * as PubSub from "effect/PubSub";40import * as PubSub from "effect/PubSub";
39import * as Ref from "effect/Ref";41import * as Ref from "effect/Ref";
40import * as Schema from "effect/Schema";42import * as Schema from "effect/Schema";
41import * as SchemaIssue from "effect/SchemaIssue";43import * as SchemaIssue from "effect/SchemaIssue";
44import * as Semaphore from "effect/Semaphore";
42import * as Scope from "effect/Scope";45import * as Scope from "effect/Scope";
43import * as Context from "effect/Context";
44import * as Stream from "effect/Stream";46import * as Stream from "effect/Stream";
45import * as Cause from "effect/Cause";
46import * as Semaphore from "effect/Semaphore";
47import { writeFileStringAtomically } from "./atomicWrite.ts";47import { writeFileStringAtomically } from "./atomicWrite.ts";
48import { ServerConfig } from "./config.ts";48import * as ServerConfig from "./config.ts";
49import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct";49import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct";
50import { fromJsonStringPretty, fromLenientJson } from "@t3tools/shared/schemaJson";50import { fromJsonStringPretty, fromLenientJson } from "@t3tools/shared/schemaJson";
51import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";51import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
...@@ -109,59 +109,60 @@ export function redactServerSettingsForClient(settings: ServerSettings): ServerS...@@ -109,59 +109,60 @@ export function redactServerSettingsForClient(settings: ServerSettings): ServerS
109 return { ...settings, providerInstances };109 return { ...settings, providerInstances };
110}110}
111111
112export interface ServerSettingsShape {112export class ServerSettingsService extends Context.Service<
113 /** Start the settings runtime and attach file watching. */113 ServerSettingsService,
114 readonly start: Effect.Effect<void, ServerSettingsError>;114 {
115115 /** Start the settings runtime and attach file watching. */
116 /** Await settings runtime readiness. */116 readonly start: Effect.Effect<void, ServerSettingsError>;
117 readonly ready: Effect.Effect<void, ServerSettingsError>;
118117
119 /** Read the current settings. */118 /** Await settings runtime readiness. */
120 readonly getSettings: Effect.Effect<ServerSettings, ServerSettingsError>;119 readonly ready: Effect.Effect<void, ServerSettingsError>;
121120
122 /** Patch settings and persist. Returns the new full settings object. */121 /** Read the current settings. */
123 readonly updateSettings: (122 readonly getSettings: Effect.Effect<ServerSettings, ServerSettingsError>;
124 patch: ServerSettingsPatch,
125 ) => Effect.Effect<ServerSettings, ServerSettingsError>;
126123
127 /** Stream of settings change events. */124 /** Patch settings and persist. Returns the new full settings object. */
128 readonly streamChanges: Stream.Stream<ServerSettings>;125 readonly updateSettings: (
129}126 patch: ServerSettingsPatch,
127 ) => Effect.Effect<ServerSettings, ServerSettingsError>;
130128
131export class ServerSettingsService extends Context.Service<129 /** Stream of settings change events. */
132 ServerSettingsService,130 readonly streamChanges: Stream.Stream<ServerSettings>;
133 ServerSettingsShape131 }
134>()("t3/serverSettings/ServerSettingsService") {132>()("t3/serverSettings/ServerSettingsService") {
135 static readonly layerTest = (overrides: DeepPartial<ServerSettings> = {}) =>133 /** @deprecated Import and use `layerTest` from this module. */
136 Layer.effect(134 static readonly layerTest = (overrides: DeepPartial<ServerSettings> = {}) => layerTest(overrides);
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 );
163}135}
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
165const ServerSettingsJson = fromLenientJson(ServerSettings);166const ServerSettingsJson = fromLenientJson(ServerSettings);
166const decodeServerSettingsJsonExit = Schema.decodeUnknownExit(ServerSettingsJson);167const decodeServerSettingsJsonExit = Schema.decodeUnknownExit(ServerSettingsJson);
167168
...@@ -255,8 +256,8 @@ function stripDefaultServerSettings(current: unknown, defaults: unknown): unknow...@@ -255,8 +256,8 @@ function stripDefaultServerSettings(current: unknown, defaults: unknown): unknow
255 return Object.is(current, defaults) ? undefined : current;256 return Object.is(current, defaults) ? undefined : current;
256}257}
257258
258const makeServerSettings = Effect.gen(function* () {259const make = Effect.gen(function* () {
259 const { settingsPath } = yield* ServerConfig;260 const { settingsPath } = yield* ServerConfig.ServerConfig;
260 const fs = yield* FileSystem.FileSystem;261 const fs = yield* FileSystem.FileSystem;
261 const pathService = yield* Path.Path;262 const pathService = yield* Path.Path;
262 const secretStore = yield* ServerSecretStore.ServerSecretStore;263 const secretStore = yield* ServerSecretStore.ServerSecretStore;
...@@ -578,9 +579,7 @@ const makeServerSettings = Effect.gen(function* () {...@@ -578,9 +579,7 @@ const makeServerSettings = Effect.gen(function* () {
578 Stream.map(resolveTextGenerationProvider),579 Stream.map(resolveTextGenerationProvider),
579 );580 );
580 },581 },
581 } satisfies ServerSettingsShape;582 } satisfies ServerSettingsService["Service"];
582});583});
583584
584export const ServerSettingsLive = Layer.effect(ServerSettingsService, makeServerSettings).pipe(585export const layer = Layer.effect(ServerSettingsService, make);
585 Layer.provide(ServerSecretStore.layer),
586);