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