diff --git a/apps/server/src/auth/EnvironmentAuth.test.ts b/apps/server/src/auth/EnvironmentAuth.test.ts index 871ec1eab60..b917cadb980 100644 --- a/apps/server/src/auth/EnvironmentAuth.test.ts +++ b/apps/server/src/auth/EnvironmentAuth.test.ts @@ -4,27 +4,26 @@ import { expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; -import type { ServerConfigShape } from "../config.ts"; -import { ServerConfig } from "../config.ts"; +import * as ServerConfig from "../config.ts"; import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; import * as PairingGrantStore from "./PairingGrantStore.ts"; import * as EnvironmentAuth from "./EnvironmentAuth.ts"; import * as ServerSecretStore from "./ServerSecretStore.ts"; -const makeServerConfigLayer = (overrides?: Partial) => +const makeServerConfigLayer = (overrides?: Partial) => Layer.effect( - ServerConfig, + ServerConfig.ServerConfig, Effect.gen(function* () { - const config = yield* ServerConfig; + const config = yield* ServerConfig.ServerConfig; return { ...config, ...overrides, - } satisfies ServerConfigShape; + } satisfies ServerConfig.ServerConfig["Service"]; }), ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-server-test-" }))); -const makeEnvironmentAuthLayer = (overrides?: Partial) => +const makeEnvironmentAuthLayer = (overrides?: Partial) => EnvironmentAuth.layer.pipe( Layer.provide(SqlitePersistenceMemory), Layer.provide(ServerSecretStore.layer), @@ -33,13 +32,15 @@ const makeEnvironmentAuthLayer = (overrides?: Partial) => const makeCookieRequest = ( sessionToken: string, -): Parameters[0] => +): Parameters[0] => ({ cookies: { t3_session: sessionToken, }, headers: {}, - }) as unknown as Parameters[0]; + }) as unknown as Parameters< + EnvironmentAuth.EnvironmentAuth["Service"]["authenticateHttpRequest"] + >[0]; const requestMetadata = { deviceType: "desktop" as const, diff --git a/apps/server/src/auth/EnvironmentAuthAdmin.test.ts b/apps/server/src/auth/EnvironmentAuthAdmin.test.ts index 44c28dea416..7dcc89761be 100644 --- a/apps/server/src/auth/EnvironmentAuthAdmin.test.ts +++ b/apps/server/src/auth/EnvironmentAuthAdmin.test.ts @@ -3,31 +3,30 @@ import { expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; -import type { ServerConfigShape } from "../config.ts"; -import { ServerConfig } from "../config.ts"; +import * as ServerConfig from "../config.ts"; import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; import * as EnvironmentAuth from "./EnvironmentAuth.ts"; import * as ServerSecretStore from "./ServerSecretStore.ts"; import * as SessionStore from "./SessionStore.ts"; const makeServerConfigLayer = ( - overrides?: Partial>, + overrides?: Partial>, ) => Layer.effect( - ServerConfig, + ServerConfig.ServerConfig, Effect.gen(function* () { - const config = yield* ServerConfig; + const config = yield* ServerConfig.ServerConfig; return { ...config, ...overrides, - } satisfies ServerConfigShape; + } satisfies ServerConfig.ServerConfig["Service"]; }), ).pipe( Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-control-plane-test-" })), ); const makeEnvironmentAuthLayer = ( - overrides?: Partial>, + overrides?: Partial>, ) => EnvironmentAuth.layer.pipe( Layer.provideMerge(ServerSecretStore.layer), diff --git a/apps/server/src/auth/EnvironmentAuthPolicy.test.ts b/apps/server/src/auth/EnvironmentAuthPolicy.test.ts index c9f5dc6230d..95269fb6c37 100644 --- a/apps/server/src/auth/EnvironmentAuthPolicy.test.ts +++ b/apps/server/src/auth/EnvironmentAuthPolicy.test.ts @@ -3,21 +3,22 @@ import { expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; -import type { ServerConfigShape } from "../config.ts"; -import { ServerConfig } from "../config.ts"; +import * as ServerConfig from "../config.ts"; import * as EnvironmentAuthPolicy from "./EnvironmentAuthPolicy.ts"; -const makeEnvironmentAuthPolicyLayer = (overrides?: Partial) => +const makeEnvironmentAuthPolicyLayer = ( + overrides?: Partial, +) => EnvironmentAuthPolicy.layer.pipe( Layer.provide( Layer.effect( - ServerConfig, + ServerConfig.ServerConfig, Effect.gen(function* () { - const config = yield* ServerConfig; + const config = yield* ServerConfig.ServerConfig; return { ...config, ...overrides, - } satisfies ServerConfigShape; + } satisfies ServerConfig.ServerConfig["Service"]; }), ).pipe( Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-policy-test-" })), diff --git a/apps/server/src/auth/PairingGrantStore.test.ts b/apps/server/src/auth/PairingGrantStore.test.ts index 3861b4fc78f..12b0060094a 100644 --- a/apps/server/src/auth/PairingGrantStore.test.ts +++ b/apps/server/src/auth/PairingGrantStore.test.ts @@ -5,29 +5,28 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as TestClock from "effect/testing/TestClock"; -import type { ServerConfigShape } from "../config.ts"; -import { ServerConfig } from "../config.ts"; +import * as ServerConfig from "../config.ts"; import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; import * as PairingGrantStore from "./PairingGrantStore.ts"; const makeServerConfigLayer = ( - overrides?: Partial>, + overrides?: Partial>, ) => Layer.effect( - ServerConfig, + ServerConfig.ServerConfig, Effect.gen(function* () { - const config = yield* ServerConfig; + const config = yield* ServerConfig.ServerConfig; return { ...config, ...overrides, - } satisfies ServerConfigShape; + } satisfies ServerConfig.ServerConfig["Service"]; }), ).pipe( Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-bootstrap-test-" })), ); const makePairingGrantStoreLayer = ( - overrides?: Partial>, + overrides?: Partial>, ) => PairingGrantStore.layer.pipe( Layer.provide(SqlitePersistenceMemory), diff --git a/apps/server/src/auth/SessionStore.test.ts b/apps/server/src/auth/SessionStore.test.ts index 00abd6b9945..967766a7a4e 100644 --- a/apps/server/src/auth/SessionStore.test.ts +++ b/apps/server/src/auth/SessionStore.test.ts @@ -5,30 +5,29 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as TestClock from "effect/testing/TestClock"; -import type { ServerConfigShape } from "../config.ts"; -import { ServerConfig } from "../config.ts"; +import * as ServerConfig from "../config.ts"; import { PersistenceSqlError } from "../persistence/Errors.ts"; import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; -import { AuthSessionRepository } from "../persistence/Services/AuthSessions.ts"; +import * as AuthSessions from "../persistence/Services/AuthSessions.ts"; import * as SessionStore from "./SessionStore.ts"; import * as ServerSecretStore from "./ServerSecretStore.ts"; const makeServerConfigLayer = ( - overrides?: Partial>, + overrides?: Partial>, ) => Layer.effect( - ServerConfig, + ServerConfig.ServerConfig, Effect.gen(function* () { - const config = yield* ServerConfig; + const config = yield* ServerConfig.ServerConfig; return { ...config, ...overrides, - } satisfies ServerConfigShape; + } satisfies ServerConfig.ServerConfig["Service"]; }), ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-session-test-" }))); const makeSessionStoreLayer = ( - overrides?: Partial>, + overrides?: Partial>, ) => SessionStore.layer.pipe( Layer.provide(SqlitePersistenceMemory), @@ -41,7 +40,7 @@ const repositoryFailure = new PersistenceSqlError({ detail: "sqlite is unavailable", }); -const failingSessionLookupRepositoryLayer = Layer.succeed(AuthSessionRepository, { +const failingSessionLookupRepositoryLayer = Layer.succeed(AuthSessions.AuthSessionRepository, { create: () => Effect.void, getById: () => Effect.fail(repositoryFailure), listActive: () => Effect.succeed([]), diff --git a/apps/server/src/bin.test.ts b/apps/server/src/bin.test.ts index 27a1d55e90d..64d366468f9 100644 --- a/apps/server/src/bin.test.ts +++ b/apps/server/src/bin.test.ts @@ -20,8 +20,8 @@ import * as TestConsole from "effect/testing/TestConsole"; import { Command } from "effect/unstable/cli"; import { cli, makeCli } from "./bin.ts"; -import { deriveServerPaths, ServerConfig, type ServerConfigShape } from "./config.ts"; -import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts"; +import * as ServerConfig from "./config.ts"; +import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts"; import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts"; import { orchestrationHttpApiLayer } from "./orchestration/http.ts"; import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts"; @@ -57,7 +57,7 @@ const captureStdout = (effect: Effect.Effect) => const makeCliTestServerConfig = (baseDir: string) => Effect.gen(function* () { - const derivedPaths = yield* deriveServerPaths(baseDir, undefined); + const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, undefined); return { logLevel: "Info", traceMinLevel: "Info", @@ -84,26 +84,23 @@ const makeCliTestServerConfig = (baseDir: string) => logWebSocketEvents: false, tailscaleServeEnabled: false, tailscaleServePort: 443, - } satisfies ServerConfigShape; + } satisfies ServerConfig.ServerConfig["Service"]; }); -const makeProjectPersistenceLayer = (config: ServerConfigShape) => +const makeProjectPersistenceLayer = (config: ServerConfig.ServerConfig["Service"]) => Layer.mergeAll( OrchestrationLayerLive.pipe( Layer.provideMerge(RepositoryIdentityResolverLive), Layer.provideMerge(SqlitePersistenceLayerLive), ), WorkspacePathsLive, - ).pipe( - Layer.provideMerge(NodeServices.layer), - Layer.provide(Layer.succeed(ServerConfig, config)), - ); + ).pipe(Layer.provideMerge(NodeServices.layer), Layer.provide(ServerConfig.layer(config))); const readPersistedSnapshot = (baseDir: string) => Effect.gen(function* () { const config = yield* makeCliTestServerConfig(baseDir); return yield* Effect.gen(function* () { - const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; + const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; return yield* projectionSnapshotQuery.getSnapshot(); }).pipe(Effect.provide(makeProjectPersistenceLayer(config))); }); @@ -133,7 +130,7 @@ const withLiveProjectCliServer = (baseDir: string, run: () => Effect.Ef }), ), Layer.provideMerge(NodeServices.layer), - Layer.provide(Layer.succeed(ServerConfig, config)), + Layer.provide(ServerConfig.layer(config)), ); return yield* Effect.scoped( @@ -238,7 +235,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => { it.effect("logs in to headless connect without enabling access", () => Effect.gen(function* () { const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-cloud-login-test-")); - const { secretsDir } = yield* deriveServerPaths(baseDir, undefined); + const { secretsDir } = yield* ServerConfig.deriveServerPaths(baseDir, undefined); mkdirSync(secretsDir, { recursive: true }); writeFileSync( join(secretsDir, "cloud-cli-oauth-token.bin"), @@ -282,7 +279,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => { it.effect("logs out of headless connect and removes the stored CLI authorization", () => Effect.gen(function* () { const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-cloud-logout-test-")); - const { secretsDir } = yield* deriveServerPaths(baseDir, undefined); + const { secretsDir } = yield* ServerConfig.deriveServerPaths(baseDir, undefined); const tokenPath = join(secretsDir, "cloud-cli-oauth-token.bin"); mkdirSync(secretsDir, { recursive: true }); writeFileSync(tokenPath, "invalid persisted token"); @@ -461,7 +458,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => { "--base-dir", baseDir, ]); - const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; + const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; const readModel = yield* projectionSnapshotQuery.getSnapshot(); const addedProject = readModel.projects.find( (project) => project.workspaceRoot === workspaceRoot && project.deletedAt === null, diff --git a/apps/server/src/cli/auth.ts b/apps/server/src/cli/auth.ts index 4f1fc48871d..1b349111811 100644 --- a/apps/server/src/cli/auth.ts +++ b/apps/server/src/cli/auth.ts @@ -18,7 +18,7 @@ import { formatPairingCredentialList, formatSessionList, } from "../cliAuthFormat.ts"; -import { ServerConfig } from "../config.ts"; +import * as ServerConfig from "../config.ts"; import { authLocationFlags, type CliAuthLocationFlags, @@ -28,7 +28,7 @@ import { const runWithEnvironmentAuth = ( flags: CliAuthLocationFlags, - run: (environmentAuth: EnvironmentAuth.EnvironmentAuthShape) => Effect.Effect, + run: (environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"]) => Effect.Effect, options?: { readonly quietLogs?: boolean; }, @@ -43,7 +43,7 @@ const runWithEnvironmentAuth = ( }).pipe( Effect.provide( Layer.mergeAll(EnvironmentAuth.runtimeLayer).pipe( - Layer.provide(Layer.succeed(ServerConfig, config)), + Layer.provide(ServerConfig.layer(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)), ), ), diff --git a/apps/server/src/cli/config.ts b/apps/server/src/cli/config.ts index 7182854e18c..7a9cd72d526 100644 --- a/apps/server/src/cli/config.ts +++ b/apps/server/src/cli/config.ts @@ -14,18 +14,10 @@ import * as SchemaTransformation from "effect/SchemaTransformation"; import { Argument, Flag } from "effect/unstable/cli"; import { readBootstrapEnvelope } from "../bootstrap.ts"; -import { - DEFAULT_PORT, - deriveServerPaths, - ensureServerDirectories, - resolveStaticDir, - RuntimeMode, - type ServerConfigShape, - type StartupPresentation, -} from "../config.ts"; +import * as ServerConfig from "../config.ts"; import { expandHomePath, resolveBaseDir } from "../os-jank.ts"; -export const modeFlag = Flag.choice("mode", RuntimeMode.literals).pipe( +export const modeFlag = Flag.choice("mode", ServerConfig.RuntimeMode.literals).pipe( Flag.withDescription("Runtime mode. `desktop` keeps loopback defaults unless overridden."), Flag.optional, ); @@ -104,7 +96,7 @@ const EnvServerConfig = Config.all({ Config.withDefault(10_000), ), otlpServiceName: Config.string("T3CODE_OTLP_SERVICE_NAME").pipe(Config.withDefault("t3-server")), - mode: Config.schema(RuntimeMode, "T3CODE_MODE").pipe( + mode: Config.schema(ServerConfig.RuntimeMode, "T3CODE_MODE").pipe( Config.option, Config.map(Option.getOrUndefined), ), @@ -139,7 +131,7 @@ const EnvServerConfig = Config.all({ }); export interface CliServerFlags { - readonly mode: Option.Option; + readonly mode: Option.Option; readonly port: Option.Option; readonly host: Option.Option; readonly baseDir: Option.Option; @@ -208,7 +200,7 @@ export const resolveServerConfig = ( flags: CliServerFlags, cliLogLevel: Option.Option, options?: { - readonly startupPresentation?: StartupPresentation; + readonly startupPresentation?: ServerConfig.StartupPresentation; readonly forceAutoBootstrapProjectFromCwd?: boolean; }, ) => @@ -238,7 +230,7 @@ export const resolveServerConfig = ( : Option.none(); const bootstrap = Option.getOrUndefined(bootstrapEnvelope); - const mode: RuntimeMode = Option.getOrElse( + const mode: ServerConfig.RuntimeMode = Option.getOrElse( resolveOptionPrecedence( normalizedFlags.mode, Option.fromUndefinedOr(env.mode), @@ -257,9 +249,9 @@ export const resolveServerConfig = ( onSome: (value) => Effect.succeed(value), onNone: () => { if (mode === "desktop") { - return Effect.succeed(DEFAULT_PORT); + return Effect.succeed(ServerConfig.DEFAULT_PORT); } - return findAvailablePort(DEFAULT_PORT); + return findAvailablePort(ServerConfig.DEFAULT_PORT); }, }, ); @@ -279,8 +271,8 @@ export const resolveServerConfig = ( const rawCwd = Option.getOrElse(normalizedFlags.cwd, () => process.cwd()); const cwd = path.resolve(yield* expandHomePath(rawCwd.trim())); yield* fs.makeDirectory(cwd, { recursive: true }); - const derivedPaths = yield* deriveServerPaths(baseDir, devUrl); - yield* ensureServerDirectories(derivedPaths); + const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, devUrl); + yield* ServerConfig.ensureServerDirectories(derivedPaths); const persistedObservabilitySettings = yield* loadPersistedObservabilitySettings( derivedPaths.settingsPath, ); @@ -330,7 +322,7 @@ export const resolveServerConfig = ( ), () => 443, ); - const staticDir = devUrl ? undefined : yield* resolveStaticDir(); + const staticDir = devUrl ? undefined : yield* ServerConfig.resolveStaticDir(); const host = Option.getOrElse( resolveOptionPrecedence( normalizedFlags.host, @@ -341,7 +333,7 @@ export const resolveServerConfig = ( ); const logLevel = Option.getOrElse(cliLogLevel, () => env.logLevel); - const config: ServerConfigShape = { + const config: ServerConfig.ServerConfig["Service"] = { logLevel, traceMinLevel: env.traceMinLevel, traceTimingEnabled: env.traceTimingEnabled, diff --git a/apps/server/src/cli/connect.ts b/apps/server/src/cli/connect.ts index 9c8fb17a18b..51582965913 100644 --- a/apps/server/src/cli/connect.ts +++ b/apps/server/src/cli/connect.ts @@ -31,9 +31,9 @@ import * as CliTokenManager from "../cloud/CliTokenManager.ts"; import { CLOUD_LINKED_USER_ID, RELAY_URL_SECRET } from "../cloud/config.ts"; import { relayUrlConfig } from "../cloud/publicConfig.ts"; import { headlessRelayClientTracingLayer } from "../cloud/relayTracing.ts"; -import { ServerConfig } from "../config.ts"; +import * as ServerConfig from "../config.ts"; import { ServerEnvironmentLive } from "../environment/Layers/ServerEnvironment.ts"; -import { ServerEnvironment } from "../environment/Services/ServerEnvironment.ts"; +import * as ServerEnvironment from "../environment/Services/ServerEnvironment.ts"; import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts"; import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts"; @@ -145,7 +145,7 @@ const reportRelayClientInstallProgress = (event: RelayClientInstallProgressEvent export const acquireRelayClientForLink = Effect.fn("cloud.cli.acquire_relay_client_for_link")( function* ( - relayClient: RelayClient.RelayClientShape, + relayClient: RelayClient.RelayClient["Service"], confirmInstall: (version: string) => Effect.Effect, reportProgress: (event: RelayClientInstallProgressEvent) => Effect.Effect, ) { @@ -164,7 +164,7 @@ export const acquireRelayClientForLink = Effect.fn("cloud.cli.acquire_relay_clie ); const withCloudCliSessionToken = ( - environmentAuth: EnvironmentAuth.EnvironmentAuthShape, + environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"], run: (token: string) => Effect.Effect, ) => Effect.acquireUseRelease( @@ -183,7 +183,7 @@ type LiveCloudActionResult = | { readonly status: "failed"; readonly cause: unknown }; const runLiveCloudUnlink = Effect.fn("cloud.cli.run_live_unlink")(function* () { - const config = yield* ServerConfig; + const config = yield* ServerConfig.ServerConfig; const runtimeState = yield* readPersistedServerRuntimeState(config.serverRuntimeStatePath); if (Option.isNone(runtimeState)) { return { status: "not-running" } satisfies LiveCloudActionResult; @@ -219,7 +219,7 @@ const unlinkRelayEnvironment = Effect.fn("cloud.cli.unlink_relay_environment")(f return { status: "not-authenticated" } satisfies RelayUnlinkResult; } - const environment = yield* ServerEnvironment; + const environment = yield* ServerEnvironment.ServerEnvironment; const environmentId = yield* environment.getEnvironmentId; const relayUrl = yield* relayUrlConfig; const httpClient = yield* HttpClient.HttpClient; @@ -285,8 +285,8 @@ const runCloudCommand = ( | FileSystem.FileSystem | HttpClient.HttpClient | Prompt.Environment - | ServerConfig - | ServerEnvironment + | ServerConfig.ServerConfig + | ServerEnvironment.ServerEnvironment >, options?: { readonly quietLogs?: boolean; @@ -305,7 +305,7 @@ const runCloudCommand = ( headlessRelayClientTracingLayer, ).pipe( Layer.provideMerge(FetchHttpClient.layer), - Layer.provideMerge(Layer.succeed(ServerConfig, config)), + Layer.provideMerge(ServerConfig.layer(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)), ); return yield* run.pipe(Effect.provide(runtimeLayer)); diff --git a/apps/server/src/cli/project.ts b/apps/server/src/cli/project.ts index 0d8e7eca15d..eec7f3f5541 100644 --- a/apps/server/src/cli/project.ts +++ b/apps/server/src/cli/project.ts @@ -26,19 +26,19 @@ import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient"; import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts"; -import { ServerConfig, type ServerConfigShape } from "../config.ts"; -import { OrchestrationEngineService } from "../orchestration/Services/OrchestrationEngine.ts"; -import { ProjectionSnapshotQuery } from "../orchestration/Services/ProjectionSnapshotQuery.ts"; +import * as ServerConfig from "../config.ts"; +import * as OrchestrationEngine from "../orchestration/Services/OrchestrationEngine.ts"; +import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts"; import { OrchestrationLayerLive } from "../orchestration/runtimeLayer.ts"; import { layerConfig as SqlitePersistenceLayerLive } from "../persistence/Layers/Sqlite.ts"; import { RepositoryIdentityResolverLive } from "../project/Layers/RepositoryIdentityResolver.ts"; -import { getAutoBootstrapDefaultModelSelection } from "../serverRuntimeStartup.ts"; +import * as ServerRuntimeStartup from "../serverRuntimeStartup.ts"; import { clearPersistedServerRuntimeState, readPersistedServerRuntimeState, } from "../serverRuntimeState.ts"; import { WorkspacePathsLive } from "../workspace/Layers/WorkspacePaths.ts"; -import { WorkspacePaths } from "../workspace/Services/WorkspacePaths.ts"; +import * as WorkspacePaths from "../workspace/Services/WorkspacePaths.ts"; import { type CliAuthLocationFlags, projectLocationFlags, resolveCliAuthConfig } from "./config.ts"; type ProjectMutationTarget = { @@ -78,7 +78,7 @@ const ProjectCliRuntimeLive = Layer.mergeAll( const PROJECT_CLI_LIVE_SERVER_TIMEOUT = Duration.seconds(1); const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError); const withProjectCliSessionToken = ( - environmentAuth: EnvironmentAuth.EnvironmentAuthShape, + environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"], run: (token: string) => Effect.Effect, ) => Effect.acquireUseRelease( @@ -123,7 +123,7 @@ const makeLiveServerClient = (origin: string) => const normalizeWorkspaceRootForProjectCommand = Effect.fn( "normalizeWorkspaceRootForProjectCommand", )(function* (workspaceRoot: string) { - const workspacePaths = yield* WorkspacePaths; + const workspacePaths = yield* WorkspacePaths.WorkspacePaths; return yield* workspacePaths.normalizeWorkspaceRoot(workspaceRoot); }); @@ -211,12 +211,15 @@ const dispatchLiveOrchestrationCommand = ( }).pipe(withProjectCliLiveServerTimeout, Effect.catch(failLiveServerRequest)); const getOfflineSnapshot = Effect.fn("getOfflineSnapshot")(function* () { - const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; + const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; return yield* projectionSnapshotQuery.getSnapshot(); }); const tryResolveLiveProjectExecutionMode = Effect.fn("tryResolveLiveProjectExecutionMode")( - function* (environmentAuth: EnvironmentAuth.EnvironmentAuthShape, config: ServerConfigShape) { + function* ( + environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"], + config: ServerConfig.ServerConfig["Service"], + ) { const runtimeState = yield* readPersistedServerRuntimeState(config.serverRuntimeStatePath); if (Option.isNone(runtimeState)) { return Option.none<{ readonly origin: string }>(); @@ -251,7 +254,11 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* ( }) => Effect.Effect< string, Error, - Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path | WorkspacePaths + | Crypto.Crypto + | FileSystem.FileSystem + | HttpClient.HttpClient + | Path.Path + | WorkspacePaths.WorkspacePaths >, ) { const logLevel = yield* GlobalFlag.LogLevel; @@ -278,13 +285,13 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* ( } const offlineRuntimeLayer = ProjectCliRuntimeLive.pipe( - Layer.provide(Layer.succeed(ServerConfig, config)), + Layer.provide(ServerConfig.layer(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)), ); return yield* Effect.gen(function* () { const snapshot = yield* getOfflineSnapshot(); - const orchestrationEngine = yield* OrchestrationEngineService; + const orchestrationEngine = yield* OrchestrationEngine.OrchestrationEngineService; const output = yield* run({ snapshot, dispatch: (command) => orchestrationEngine.dispatch(command), @@ -296,7 +303,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* ( Effect.provide( Layer.mergeAll(EnvironmentAuth.runtimeLayer, WorkspacePathsLive).pipe( Layer.provideMerge(FetchHttpClient.layer), - Layer.provide(Layer.succeed(ServerConfig, config)), + Layer.provide(ServerConfig.layer(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)), ), ), @@ -341,7 +348,7 @@ const projectAddCommand = Command.make("add", { projectId, title, workspaceRoot, - defaultModelSelection: getAutoBootstrapDefaultModelSelection(), + defaultModelSelection: ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(), createdAt: DateTime.formatIso(yield* DateTime.now), }); return `Added project ${projectId} (${title}) at ${workspaceRoot}.`; diff --git a/apps/server/src/config.ts b/apps/server/src/config.ts index b0a23cb273c..2608ccc16ae 100644 --- a/apps/server/src/config.ts +++ b/apps/server/src/config.ts @@ -6,13 +6,13 @@ * * @module ServerConfig */ +import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as LogLevel from "effect/LogLevel"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; -import * as Context from "effect/Context"; export const DEFAULT_PORT = 3773; @@ -46,38 +46,51 @@ export interface ServerDerivedPaths { } /** - * ServerConfigShape - Process/runtime configuration required by the server. + * ServerConfig - Service tag for server runtime configuration. */ -export interface ServerConfigShape extends ServerDerivedPaths { - readonly logLevel: LogLevel.LogLevel; - readonly traceMinLevel: LogLevel.LogLevel; - readonly traceTimingEnabled: boolean; - readonly traceBatchWindowMs: number; - readonly traceMaxBytes: number; - readonly traceMaxFiles: number; - readonly otlpTracesUrl: string | undefined; - readonly otlpMetricsUrl: string | undefined; - readonly otlpExportIntervalMs: number; - readonly otlpServiceName: string; - readonly mode: RuntimeMode; - readonly port: number; - readonly host: string | undefined; - readonly cwd: string; - readonly baseDir: string; - readonly staticDir: string | undefined; - readonly devUrl: URL | undefined; - readonly noBrowser: boolean; - readonly startupPresentation: StartupPresentation; - readonly desktopBootstrapToken: string | undefined; - readonly autoBootstrapProjectFromCwd: boolean; - readonly logWebSocketEvents: boolean; - readonly tailscaleServeEnabled: boolean; - readonly tailscaleServePort: number; +export class ServerConfig extends Context.Service< + ServerConfig, + ServerDerivedPaths & { + readonly logLevel: LogLevel.LogLevel; + readonly traceMinLevel: LogLevel.LogLevel; + readonly traceTimingEnabled: boolean; + readonly traceBatchWindowMs: number; + readonly traceMaxBytes: number; + readonly traceMaxFiles: number; + readonly otlpTracesUrl: string | undefined; + readonly otlpMetricsUrl: string | undefined; + readonly otlpExportIntervalMs: number; + readonly otlpServiceName: string; + readonly mode: RuntimeMode; + readonly port: number; + readonly host: string | undefined; + readonly cwd: string; + readonly baseDir: string; + readonly staticDir: string | undefined; + readonly devUrl: URL | undefined; + readonly noBrowser: boolean; + readonly startupPresentation: StartupPresentation; + readonly desktopBootstrapToken: string | undefined; + readonly autoBootstrapProjectFromCwd: boolean; + readonly logWebSocketEvents: boolean; + readonly tailscaleServeEnabled: boolean; + readonly tailscaleServePort: number; + } +>()("t3/config/ServerConfig") { + /** @deprecated Import and use `layerTest` from this module. */ + static readonly layerTest = ( + cwd: string, + baseDirOrPrefix: string | { readonly prefix: string }, + ) => layerTest(cwd, baseDirOrPrefix); } +export const make = (config: ServerConfig["Service"]) => ServerConfig.of(config); + +export const layer = (config: ServerConfig["Service"]) => Layer.succeed(ServerConfig, make(config)); + export const deriveServerPaths = Effect.fn(function* ( - baseDir: ServerConfigShape["baseDir"], - devUrl: ServerConfigShape["devUrl"], + baseDir: ServerConfig["Service"]["baseDir"], + devUrl: ServerConfig["Service"]["devUrl"], ): Effect.fn.Return { const { join } = yield* Path.Path; const stateDir = join(baseDir, devUrl !== undefined ? "dev" : "userdata"); @@ -129,56 +142,50 @@ export const ensureServerDirectories = Effect.fn(function* (derivedPaths: Server ); }); -/** - * ServerConfig - Service tag for server runtime configuration. - */ -export class ServerConfig extends Context.Service()( - "t3/config/ServerConfig", +const makeTest = Effect.fn("ServerConfig.makeTest")(function* ( + cwd: string, + baseDirOrPrefix: string | { readonly prefix: string }, ) { - static readonly layerTest = (cwd: string, baseDirOrPrefix: string | { prefix: string }) => - Layer.effect( - ServerConfig, - Effect.gen(function* () { - const devUrl = undefined; + const devUrl = undefined; + const fs = yield* FileSystem.FileSystem; + const baseDir = + typeof baseDirOrPrefix === "string" + ? baseDirOrPrefix + : yield* fs.makeTempDirectoryScoped({ prefix: baseDirOrPrefix.prefix }); + const derivedPaths = yield* deriveServerPaths(baseDir, devUrl); + yield* ensureServerDirectories(derivedPaths); - const fs = yield* FileSystem.FileSystem; - const baseDir = - typeof baseDirOrPrefix === "string" - ? baseDirOrPrefix - : yield* fs.makeTempDirectoryScoped({ prefix: baseDirOrPrefix.prefix }); - const derivedPaths = yield* deriveServerPaths(baseDir, devUrl); - yield* ensureServerDirectories(derivedPaths); + return ServerConfig.of({ + logLevel: "Error", + traceMinLevel: "Info", + traceTimingEnabled: true, + traceBatchWindowMs: 200, + traceMaxBytes: 10 * 1024 * 1024, + traceMaxFiles: 10, + otlpTracesUrl: undefined, + otlpMetricsUrl: undefined, + otlpExportIntervalMs: 10_000, + otlpServiceName: "t3-server", + cwd, + baseDir, + ...derivedPaths, + mode: "web", + autoBootstrapProjectFromCwd: false, + logWebSocketEvents: false, + tailscaleServeEnabled: false, + tailscaleServePort: 443, + port: 0, + host: undefined, + desktopBootstrapToken: undefined, + staticDir: undefined, + devUrl, + noBrowser: false, + startupPresentation: "browser", + }); +}); - return { - logLevel: "Error", - traceMinLevel: "Info", - traceTimingEnabled: true, - traceBatchWindowMs: 200, - traceMaxBytes: 10 * 1024 * 1024, - traceMaxFiles: 10, - otlpTracesUrl: undefined, - otlpMetricsUrl: undefined, - otlpExportIntervalMs: 10_000, - otlpServiceName: "t3-server", - cwd, - baseDir, - ...derivedPaths, - mode: "web", - autoBootstrapProjectFromCwd: false, - logWebSocketEvents: false, - tailscaleServeEnabled: false, - tailscaleServePort: 443, - port: 0, - host: undefined, - desktopBootstrapToken: undefined, - staticDir: undefined, - devUrl, - noBrowser: false, - startupPresentation: "browser", - } satisfies ServerConfigShape; - }), - ); -} +export const layerTest = (cwd: string, baseDirOrPrefix: string | { readonly prefix: string }) => + Layer.effect(ServerConfig, makeTest(cwd, baseDirOrPrefix)); export const resolveStaticDir = Effect.fn(function* () { const { join, resolve } = yield* Path.Path; diff --git a/apps/server/src/environment/Layers/ServerEnvironment.test.ts b/apps/server/src/environment/Layers/ServerEnvironment.test.ts index 6904c53c847..3bb96a83e1c 100644 --- a/apps/server/src/environment/Layers/ServerEnvironment.test.ts +++ b/apps/server/src/environment/Layers/ServerEnvironment.test.ts @@ -8,15 +8,15 @@ import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as PlatformError from "effect/PlatformError"; -import { deriveServerPaths, ServerConfig, type ServerConfigShape } from "../../config.ts"; -import { ServerEnvironment } from "../Services/ServerEnvironment.ts"; +import * as ServerConfig from "../../config.ts"; +import * as ServerEnvironment from "../Services/ServerEnvironment.ts"; import { ServerEnvironmentLive } from "./ServerEnvironment.ts"; const makeServerEnvironmentLayer = (baseDir: string) => ServerEnvironmentLive.pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir))); const makeServerConfig = Effect.fn(function* (baseDir: string) { - const derivedPaths = yield* deriveServerPaths(baseDir, undefined); + const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, undefined); return { ...derivedPaths, @@ -44,7 +44,7 @@ const makeServerConfig = Effect.fn(function* (baseDir: string) { devUrl: undefined, noBrowser: false, startupPresentation: "browser", - } satisfies ServerConfigShape; + } satisfies ServerConfig.ServerConfig["Service"]; }); it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => { @@ -56,11 +56,11 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => { }); const first = yield* Effect.gen(function* () { - const serverEnvironment = yield* ServerEnvironment; + const serverEnvironment = yield* ServerEnvironment.ServerEnvironment; return yield* serverEnvironment.getDescriptor; }).pipe(Effect.provide(makeServerEnvironmentLayer(baseDir))); const second = yield* Effect.gen(function* () { - const serverEnvironment = yield* ServerEnvironment; + const serverEnvironment = yield* ServerEnvironment.ServerEnvironment; return yield* serverEnvironment.getDescriptor; }).pipe(Effect.provide(makeServerEnvironmentLayer(baseDir))); @@ -109,14 +109,12 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => { }); const exit = yield* Effect.gen(function* () { - const serverEnvironment = yield* ServerEnvironment; + const serverEnvironment = yield* ServerEnvironment.ServerEnvironment; return yield* serverEnvironment.getDescriptor; }).pipe( Effect.provide( ServerEnvironmentLive.pipe( - Layer.provide( - Layer.merge(Layer.succeed(ServerConfig, serverConfig), failingFileSystemLayer), - ), + Layer.provide(Layer.merge(ServerConfig.layer(serverConfig), failingFileSystemLayer)), ), ), Effect.exit, diff --git a/apps/server/src/keybindings.test.ts b/apps/server/src/keybindings.test.ts index 1bfd042d078..ba95422735c 100644 --- a/apps/server/src/keybindings.test.ts +++ b/apps/server/src/keybindings.test.ts @@ -9,28 +9,21 @@ import * as Layer from "effect/Layer"; import * as Logger from "effect/Logger"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; -import { ServerConfig } from "./config.ts"; - -import { - DEFAULT_KEYBINDINGS, - Keybindings, - KeybindingsLive, - ResolvedKeybindingFromConfig, - compileResolvedKeybindingRule, - compileResolvedKeybindingsConfig, - parseKeybindingShortcut, -} from "./keybindings.ts"; +import * as ServerConfig from "./config.ts"; +import * as Keybindings from "./keybindings.ts"; import { KeybindingsConfigError } from "@t3tools/contracts"; const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig); const encodeKeybindingsConfigJson = Schema.encodeEffect(KeybindingsConfigJson); const decodeKeybindingsConfigJson = Schema.decodeUnknownEffect(KeybindingsConfigJson); -const encodeResolvedKeybindingFromConfig = Schema.encodeEffect(ResolvedKeybindingFromConfig); +const encodeResolvedKeybindingFromConfig = Schema.encodeEffect( + Keybindings.ResolvedKeybindingFromConfig, +); const decodeResolvedKeybindingFromConfigExit = Schema.decodeUnknownExit( - ResolvedKeybindingFromConfig, + Keybindings.ResolvedKeybindingFromConfig, ); const makeKeybindingsLayer = () => { - return KeybindingsLive.pipe( + return Keybindings.layer.pipe( Layer.provideMerge( Layer.fresh( ServerConfig.layerTest(process.cwd(), { @@ -66,7 +59,7 @@ const readKeybindingsConfig = (configPath: string) => it.layer(NodeServices.layer)("keybindings", (it) => { it.effect("parses shortcuts including plus key", () => Effect.sync(() => { - assert.deepEqual(parseKeybindingShortcut("mod+j"), { + assert.deepEqual(Keybindings.parseKeybindingShortcut("mod+j"), { key: "j", metaKey: false, ctrlKey: false, @@ -74,7 +67,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { altKey: false, modKey: true, }); - assert.deepEqual(parseKeybindingShortcut("mod++"), { + assert.deepEqual(Keybindings.parseKeybindingShortcut("mod++"), { key: "+", metaKey: false, ctrlKey: false, @@ -87,7 +80,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { it.effect("compiles valid rule with parsed when AST", () => Effect.sync(() => { - const compiled = compileResolvedKeybindingRule({ + const compiled = Keybindings.compileResolvedKeybindingRule({ key: "mod+d", command: "terminal.split", when: "terminalOpen && !terminalFocus", @@ -137,14 +130,14 @@ it.layer(NodeServices.layer)("keybindings", (it) => { it.effect("rejects invalid rules", () => Effect.sync(() => { assert.isNull( - compileResolvedKeybindingRule({ + Keybindings.compileResolvedKeybindingRule({ key: "mod+shift+d+o", command: "terminal.new", }), ); assert.isNull( - compileResolvedKeybindingRule({ + Keybindings.compileResolvedKeybindingRule({ key: "mod+d", command: "terminal.split", when: "terminalFocus && (", @@ -152,7 +145,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { ); assert.isNull( - compileResolvedKeybindingRule({ + Keybindings.compileResolvedKeybindingRule({ key: "mod+d", command: "terminal.split", when: `${"!".repeat(300)}terminalFocus`, @@ -181,23 +174,23 @@ it.layer(NodeServices.layer)("keybindings", (it) => { it.effect("bootstraps default keybindings when config file is missing", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; - const { keybindingsConfigPath } = yield* ServerConfig; + const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; assert.isFalse(yield* fs.exists(keybindingsConfigPath)); yield* Effect.gen(function* () { - const keybindings = yield* Keybindings; + const keybindings = yield* Keybindings.Keybindings; yield* keybindings.syncDefaultKeybindingsOnStartup; }); const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); - assert.deepEqual(persisted, DEFAULT_KEYBINDINGS); + assert.deepEqual(persisted, Keybindings.DEFAULT_KEYBINDINGS); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("ships configurable thread navigation defaults", () => Effect.sync(() => { const defaultsByCommand = new Map( - DEFAULT_KEYBINDINGS.map((binding) => [binding.command, binding.key] as const), + Keybindings.DEFAULT_KEYBINDINGS.map((binding) => [binding.command, binding.key] as const), ); assert.equal(defaultsByCommand.get("thread.previous"), "mod+shift+["); @@ -215,17 +208,17 @@ it.layer(NodeServices.layer)("keybindings", (it) => { it.effect("uses defaults in runtime when config is malformed without overriding file", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; - const { keybindingsConfigPath } = yield* ServerConfig; + const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* fs.writeFileString(keybindingsConfigPath, "{ not-json"); const configState = yield* Effect.gen(function* () { - const keybindings = yield* Keybindings; + const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.loadConfigState; }); assert.deepEqual( configState.keybindings, - compileResolvedKeybindingsConfig(DEFAULT_KEYBINDINGS), + Keybindings.compileResolvedKeybindingsConfig(Keybindings.DEFAULT_KEYBINDINGS), ); assert.deepEqual(configState.issues, [ { @@ -240,7 +233,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { it.effect("ignores invalid entries in runtime and reports them as issues", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; - const { keybindingsConfigPath } = yield* ServerConfig; + const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* fs.writeFileString( keybindingsConfigPath, // @effect-diagnostics-next-line preferSchemaOverJson:off @@ -252,7 +245,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { ); const configState = yield* Effect.gen(function* () { - const keybindings = yield* Keybindings; + const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.loadConfigState; }); @@ -279,14 +272,14 @@ it.layer(NodeServices.layer)("keybindings", (it) => { "upserts missing default keybindings on startup without overriding existing command rules", () => Effect.gen(function* () { - const { keybindingsConfigPath } = yield* ServerConfig; + const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+shift+t", command: "terminal.toggle" }, { key: "mod+shift+r", command: "script.run-tests.run" }, ]); yield* Effect.gen(function* () { - const keybindings = yield* Keybindings; + const keybindings = yield* Keybindings.Keybindings; yield* keybindings.syncDefaultKeybindingsOnStartup; }); @@ -300,7 +293,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { persisted.some((entry) => entry.command === "terminal.toggle" && entry.key === "mod+j"), ); - for (const defaultRule of DEFAULT_KEYBINDINGS) { + for (const defaultRule of Keybindings.DEFAULT_KEYBINDINGS) { assert.isTrue(byCommand.has(defaultRule.command), `expected ${defaultRule.command}`); } assert.isTrue(byCommand.has("script.run-tests.run")); @@ -314,13 +307,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => { }); return Effect.gen(function* () { - const { keybindingsConfigPath } = yield* ServerConfig; + const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+j", command: "script.custom-action.run" }, ]); yield* Effect.gen(function* () { - const keybindings = yield* Keybindings; + const keybindings = yield* Keybindings.Keybindings; yield* keybindings.syncDefaultKeybindingsOnStartup; }); @@ -345,13 +338,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => { it.effect("upserts custom keybindings to configured path", () => Effect.gen(function* () { - const { keybindingsConfigPath } = yield* ServerConfig; + const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+j", command: "terminal.toggle" }, ]); const resolved = yield* Effect.gen(function* () { - const keybindings = yield* Keybindings; + const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+shift+r", command: "script.run-tests.run", @@ -371,12 +364,12 @@ it.layer(NodeServices.layer)("keybindings", (it) => { it.effect("appends additional custom keybindings for the same command", () => Effect.gen(function* () { - const { keybindingsConfigPath } = yield* ServerConfig; + const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+r", command: "script.run-tests.run" }, ]); yield* Effect.gen(function* () { - const keybindings = yield* Keybindings; + const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+shift+r", command: "script.run-tests.run", @@ -394,13 +387,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => { it.effect("replaces only the targeted custom keybinding", () => Effect.gen(function* () { - const { keybindingsConfigPath } = yield* ServerConfig; + const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+r", command: "script.run-tests.run" }, { key: "mod+shift+r", command: "script.run-tests.run" }, ]); yield* Effect.gen(function* () { - const keybindings = yield* Keybindings; + const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+alt+r", command: "script.run-tests.run", @@ -419,13 +412,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => { it.effect("removes only the targeted custom keybinding", () => Effect.gen(function* () { - const { keybindingsConfigPath } = yield* ServerConfig; + const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+r", command: "script.run-tests.run" }, { key: "mod+shift+r", command: "script.run-tests.run" }, ]); yield* Effect.gen(function* () { - const keybindings = yield* Keybindings; + const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.removeKeybindingRule({ key: "mod+r", command: "script.run-tests.run", @@ -441,11 +434,11 @@ it.layer(NodeServices.layer)("keybindings", (it) => { it.effect("refuses to overwrite malformed keybindings config", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; - const { keybindingsConfigPath } = yield* ServerConfig; + const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* fs.writeFileString(keybindingsConfigPath, "{ not-json"); const result = yield* Effect.gen(function* () { - const keybindings = yield* Keybindings; + const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+shift+r", command: "script.run-tests.run", @@ -461,14 +454,14 @@ it.layer(NodeServices.layer)("keybindings", (it) => { it.effect("reports non-array config parse errors without duplicate prefix", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; - const { keybindingsConfigPath } = yield* ServerConfig; + const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* fs.writeFileString( keybindingsConfigPath, '{"key":"mod+j","command":"terminal.toggle"}', ); const firstResult = yield* Effect.gen(function* () { - const keybindings = yield* Keybindings; + const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+shift+r", command: "script.run-tests.run", @@ -477,7 +470,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { assertFailure(firstResult, "expected JSON array"); const secondResult = yield* Effect.gen(function* () { - const keybindings = yield* Keybindings; + const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+shift+r", command: "script.run-tests.run", @@ -490,7 +483,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { it.effect("fails when config directory is not writable", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; - const { keybindingsConfigPath } = yield* ServerConfig; + const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; const { dirname } = yield* Path.Path; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+j", command: "terminal.toggle" }, @@ -498,7 +491,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { yield* fs.chmod(dirname(keybindingsConfigPath), 0o500); const result = yield* Effect.gen(function* () { - const keybindings = yield* Keybindings; + const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+shift+r", command: "script.run-tests.run", @@ -516,13 +509,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => { it.effect("caches loaded resolved config across repeated reads", () => Effect.gen(function* () { - const { keybindingsConfigPath } = yield* ServerConfig; + const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+j", command: "terminal.toggle" }, ]); const [first, second] = yield* Effect.gen(function* () { - const keybindings = yield* Keybindings; + const keybindings = yield* Keybindings.Keybindings; const firstLoad = (yield* keybindings.loadConfigState).keybindings; const secondLoad = (yield* keybindings.loadConfigState).keybindings; return [firstLoad, secondLoad] as const; @@ -535,13 +528,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => { it.effect("updates cached resolved config after upsert", () => Effect.gen(function* () { - const { keybindingsConfigPath } = yield* ServerConfig; + const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+j", command: "terminal.toggle" }, ]); const loadedAfterUpsert = yield* Effect.gen(function* () { - const keybindings = yield* Keybindings; + const keybindings = yield* Keybindings.Keybindings; yield* keybindings.loadConfigState; yield* keybindings.upsertKeybindingRule({ key: "mod+shift+r", @@ -557,7 +550,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { it.effect("serializes concurrent upserts to avoid lost updates", () => Effect.gen(function* () { - const { keybindingsConfigPath } = yield* ServerConfig; + const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, []); const commands = Array.from( @@ -565,7 +558,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { (_, index): KeybindingCommand => `script.concurrent-${index}.run`, ); yield* Effect.gen(function* () { - const keybindings = yield* Keybindings; + const keybindings = yield* Keybindings.Keybindings; yield* Effect.all( commands.map((command, index) => keybindings.upsertKeybindingRule({ diff --git a/apps/server/src/keybindings.ts b/apps/server/src/keybindings.ts index 80b522eee71..5ddae4943f8 100644 --- a/apps/server/src/keybindings.ts +++ b/apps/server/src/keybindings.ts @@ -41,7 +41,7 @@ import * as Context from "effect/Context"; import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; import * as Semaphore from "effect/Semaphore"; -import { ServerConfig } from "./config.ts"; +import * as ServerConfig from "./config.ts"; import { writeFileStringAtomically } from "./atomicWrite.ts"; import { fromJsonStringPretty, fromLenientJson } from "@t3tools/shared/schemaJson"; import { @@ -225,74 +225,70 @@ function mergeWithDefaultKeybindings(custom: ResolvedKeybindingsConfig): Resolve return merged.slice(-MAX_KEYBINDINGS_COUNT); } -/** - * KeybindingsShape - Service API for keybinding configuration operations. - */ -export interface KeybindingsShape { - /** - * Start the keybindings runtime and attach file watching. - * - * Safe to call multiple times. The first successful call establishes the - * runtime; later calls await the same startup. - */ - readonly start: Effect.Effect; - - /** - * Await keybindings runtime readiness. - * - * Readiness means the config directory exists, the watcher is attached, the - * startup sync has completed, and the current snapshot has been loaded. - */ - readonly ready: Effect.Effect; - - /** - * Ensure the on-disk keybindings file exists and includes all default - * commands so newly-added defaults are backfilled on startup. - */ - readonly syncDefaultKeybindingsOnStartup: Effect.Effect; - - /** - * Load runtime keybindings state along with non-fatal configuration issues. - */ - readonly loadConfigState: Effect.Effect; - - /** - * Read the latest keybindings snapshot from cache/disk. - */ - readonly getSnapshot: Effect.Effect; - - /** - * Stream of keybindings config change events. - */ - readonly streamChanges: Stream.Stream; - - /** - * Upsert a keybinding rule and persist the resulting configuration. - * - * Writes config atomically and enforces the max rule count by truncating - * oldest entries when needed. - */ - readonly upsertKeybindingRule: ( - input: ServerUpsertKeybindingInput, - ) => Effect.Effect; - - /** - * Remove a single persisted keybinding rule by exact key/command/when match. - */ - readonly removeKeybindingRule: ( - input: ServerRemoveKeybindingInput, - ) => Effect.Effect; -} - /** * Keybindings - Service tag for keybinding configuration operations. */ -export class Keybindings extends Context.Service()( - "t3/keybindings", -) {} +export class Keybindings extends Context.Service< + Keybindings, + { + /** + * Start the keybindings runtime and attach file watching. + * + * Safe to call multiple times. The first successful call establishes the + * runtime; later calls await the same startup. + */ + readonly start: Effect.Effect; + + /** + * Await keybindings runtime readiness. + * + * Readiness means the config directory exists, the watcher is attached, the + * startup sync has completed, and the current snapshot has been loaded. + */ + readonly ready: Effect.Effect; + + /** + * Ensure the on-disk keybindings file exists and includes all default + * commands so newly-added defaults are backfilled on startup. + */ + readonly syncDefaultKeybindingsOnStartup: Effect.Effect; + + /** + * Load runtime keybindings state along with non-fatal configuration issues. + */ + readonly loadConfigState: Effect.Effect; + + /** + * Read the latest keybindings snapshot from cache/disk. + */ + readonly getSnapshot: Effect.Effect; + + /** + * Stream of keybindings config change events. + */ + readonly streamChanges: Stream.Stream; + + /** + * Upsert a keybinding rule and persist the resulting configuration. + * + * Writes config atomically and enforces the max rule count by truncating + * oldest entries when needed. + */ + readonly upsertKeybindingRule: ( + input: ServerUpsertKeybindingInput, + ) => Effect.Effect; + + /** + * Remove a single persisted keybinding rule by exact key/command/when match. + */ + readonly removeKeybindingRule: ( + input: ServerRemoveKeybindingInput, + ) => Effect.Effect; + } +>()("t3/keybindings") {} -const makeKeybindings = Effect.gen(function* () { - const { keybindingsConfigPath } = yield* ServerConfig; +const make = Effect.gen(function* () { + const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const upsertSemaphore = yield* Semaphore.make(1); @@ -700,7 +696,7 @@ const makeKeybindings = Effect.gen(function* () { return nextResolved; }), ), - } satisfies KeybindingsShape; + } satisfies Keybindings["Service"]; }); -export const KeybindingsLive = Layer.effect(Keybindings, makeKeybindings); +export const layer = Layer.effect(Keybindings, make); diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index 5fe0f903686..1805b6ed277 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -32,8 +32,8 @@ import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings"; import { checkCodexProviderStatus, type CodexAppServerProviderSnapshot } from "./CodexProvider.ts"; import { checkClaudeProviderStatus } from "./ClaudeProvider.ts"; -import { OpenCodeRuntimeLive } from "../opencodeRuntime.ts"; -import { NoOpProviderEventLoggers, ProviderEventLoggers } from "./ProviderEventLoggers.ts"; +import * as OpenCodeRuntime from "../opencodeRuntime.ts"; +import * as ProviderEventLoggers from "./ProviderEventLoggers.ts"; import { ProviderInstanceRegistryHydrationLive } from "./ProviderInstanceRegistryHydration.ts"; import { haveProvidersChanged, @@ -42,12 +42,12 @@ import { ProviderRegistryLive, selectProvidersByKind, } from "./ProviderRegistry.ts"; -import { ServerConfig } from "../../config.ts"; -import { ServerSettingsService, type ServerSettingsShape } from "../../serverSettings.ts"; +import * as ServerConfig from "../../config.ts"; +import * as ServerSettingsModule from "../../serverSettings.ts"; import { readProviderStatusCache, resolveProviderStatusCachePath } from "../providerStatusCache.ts"; import type { ProviderInstance } from "../ProviderDriver.ts"; -import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts"; -import { ProviderRegistry } from "../Services/ProviderRegistry.ts"; +import * as ProviderInstanceRegistry from "../Services/ProviderInstanceRegistry.ts"; +import * as ProviderRegistry from "../Services/ProviderRegistry.ts"; import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts"; const decodeServerSettings = Schema.decodeSync(ServerSettings); const encodeServerSettings = Schema.encodeSync(ServerSettings); @@ -294,11 +294,11 @@ function makeMutableServerSettingsService( get streamChanges() { return Stream.fromPubSub(changes); }, - } satisfies ServerSettingsShape; + } satisfies ServerSettingsModule.ServerSettingsService["Service"]; }); } -it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), TestHttpClientLive))( +it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), TestHttpClientLive))( "ProviderRegistry", (it) => { describe("checkCodexProviderStatus", () => { @@ -636,14 +636,17 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T adapter: {} as ProviderInstance["adapter"], textGeneration: {} as ProviderInstance["textGeneration"], } satisfies ProviderInstance; - const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, { - getInstance: (instanceId) => - Effect.succeed(instanceId === codexInstanceId ? instance : undefined), - listInstances: Effect.succeed([instance]), - listUnavailable: Effect.succeed([]), - streamChanges: Stream.empty, - subscribeChanges: Effect.flatMap(PubSub.unbounded(), PubSub.subscribe), - }); + const instanceRegistryLayer = Layer.succeed( + ProviderInstanceRegistry.ProviderInstanceRegistry, + { + getInstance: (instanceId) => + Effect.succeed(instanceId === codexInstanceId ? instance : undefined), + listInstances: Effect.succeed([instance]), + listUnavailable: Effect.succeed([]), + streamChanges: Stream.empty, + subscribeChanges: Effect.flatMap(PubSub.unbounded(), PubSub.subscribe), + }, + ); const scope = yield* Scope.make(); yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void)); const runtimeServices = yield* Layer.build( @@ -658,7 +661,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T ), ).pipe(Scope.provide(scope)); yield* Effect.gen(function* () { - const registry = yield* ProviderRegistry; + const registry = yield* ProviderRegistry.ProviderRegistry; assert.deepStrictEqual(yield* registry.getProviders, [initialProvider]); assert.strictEqual(yield* Ref.get(refreshCalls), 0); }).pipe(Effect.provide(runtimeServices)); @@ -786,16 +789,19 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T adapter: {} as ProviderInstance["adapter"], textGeneration: {} as ProviderInstance["textGeneration"], } satisfies ProviderInstance; - const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, { - getInstance: (instanceId) => - Effect.succeed(instanceId === cursorInstanceId ? instance : undefined), - listInstances: Effect.succeed([instance]), - listUnavailable: Effect.succeed([]), - streamChanges: Stream.empty, - subscribeChanges: Effect.flatMap(PubSub.unbounded(), (pubsub) => - PubSub.subscribe(pubsub), - ), - }); + const instanceRegistryLayer = Layer.succeed( + ProviderInstanceRegistry.ProviderInstanceRegistry, + { + getInstance: (instanceId) => + Effect.succeed(instanceId === cursorInstanceId ? instance : undefined), + listInstances: Effect.succeed([instance]), + listUnavailable: Effect.succeed([]), + streamChanges: Stream.empty, + subscribeChanges: Effect.flatMap(PubSub.unbounded(), (pubsub) => + PubSub.subscribe(pubsub), + ), + }, + ); const scope = yield* Scope.make(); yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void)); const runtimeServices = yield* Layer.build( @@ -811,8 +817,8 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T ).pipe(Scope.provide(scope)); yield* Effect.gen(function* () { - const registry = yield* ProviderRegistry; - const config = yield* ServerConfig; + const registry = yield* ProviderRegistry.ProviderRegistry; + const config = yield* ServerConfig.ServerConfig; const filePath = yield* resolveProviderStatusCachePath({ cacheDir: config.providerStatusCacheDir, instanceId: cursorInstanceId, @@ -880,16 +886,19 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T adapter: {} as ProviderInstance["adapter"], textGeneration: {} as ProviderInstance["textGeneration"], } satisfies ProviderInstance; - const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, { - getInstance: (instanceId) => - Effect.succeed(instanceId === codexInstanceId ? instance : undefined), - listInstances: Effect.succeed([instance]), - listUnavailable: Effect.succeed([]), - streamChanges: Stream.empty, - subscribeChanges: Effect.flatMap(PubSub.unbounded(), (pubsub) => - PubSub.subscribe(pubsub), - ), - }); + const instanceRegistryLayer = Layer.succeed( + ProviderInstanceRegistry.ProviderInstanceRegistry, + { + getInstance: (instanceId) => + Effect.succeed(instanceId === codexInstanceId ? instance : undefined), + listInstances: Effect.succeed([instance]), + listUnavailable: Effect.succeed([]), + streamChanges: Stream.empty, + subscribeChanges: Effect.flatMap(PubSub.unbounded(), (pubsub) => + PubSub.subscribe(pubsub), + ), + }, + ); const scope = yield* Scope.make(); yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void)); const runtimeServices = yield* Layer.build( @@ -905,7 +914,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T ).pipe(Scope.provide(scope)); yield* Effect.gen(function* () { - const registry = yield* ProviderRegistry; + const registry = yield* ProviderRegistry.ProviderRegistry; assert.deepStrictEqual(yield* registry.getProviders, [cachedProvider]); assert.deepStrictEqual(yield* registry.refresh(codexDriver), [cachedProvider]); @@ -975,25 +984,28 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T const instancesRef = yield* Ref.make>([codexInstance]); const failNextList = yield* Ref.make(false); const wait = () => Effect.yieldNow; - const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, { - getInstance: (instanceId) => - Ref.get(instancesRef).pipe( - Effect.map((instances) => - instances.find((instance) => instance.instanceId === instanceId), + const instanceRegistryLayer = Layer.succeed( + ProviderInstanceRegistry.ProviderInstanceRegistry, + { + getInstance: (instanceId) => + Ref.get(instancesRef).pipe( + Effect.map((instances) => + instances.find((instance) => instance.instanceId === instanceId), + ), ), - ), - listInstances: Effect.gen(function* () { - const shouldFail = yield* Ref.get(failNextList); - if (shouldFail) { - yield* Ref.set(failNextList, false); - return yield* Effect.die(new Error("simulated registry list failure")); - } - return yield* Ref.get(instancesRef); - }), - listUnavailable: Effect.succeed([]), - streamChanges: Stream.fromPubSub(changes), - subscribeChanges: PubSub.subscribe(changes), - }); + listInstances: Effect.gen(function* () { + const shouldFail = yield* Ref.get(failNextList); + if (shouldFail) { + yield* Ref.set(failNextList, false); + return yield* Effect.die(new Error("simulated registry list failure")); + } + return yield* Ref.get(instancesRef); + }), + listUnavailable: Effect.succeed([]), + streamChanges: Stream.fromPubSub(changes), + subscribeChanges: PubSub.subscribe(changes), + }, + ); const scope = yield* Scope.make(); yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void)); const runtimeServices = yield* Layer.build( @@ -1009,7 +1021,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T ).pipe(Scope.provide(scope)); yield* Effect.gen(function* () { - const registry = yield* ProviderRegistry; + const registry = yield* ProviderRegistry.ProviderRegistry; assert.deepStrictEqual(yield* registry.getProviders, [codexProvider]); yield* Ref.set(failNextList, true); @@ -1092,15 +1104,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void)); const providerRegistryLayer = ProviderRegistryLive.pipe( Layer.provideMerge(ProviderInstanceRegistryHydrationLive), - Layer.provideMerge(Layer.succeed(ServerSettingsService, serverSettings)), + Layer.provideMerge( + Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings), + ), Layer.provideMerge( ServerConfig.layerTest(process.cwd(), { prefix: "t3-provider-registry-", }), ), Layer.provideMerge(TestHttpClientLive), - Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)), - Layer.provideMerge(OpenCodeRuntimeLive), + Layer.provideMerge( + Layer.succeed( + ProviderEventLoggers.ProviderEventLoggers, + ProviderEventLoggers.NoOpProviderEventLoggers, + ), + ), + Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive), // NO spawner mock — `ChildProcessSpawner` is supplied by the // outer `NodeServices.layer` on `it.layer(...)` and will // genuinely spawn a subprocess. The missing-binary ENOENT is @@ -1112,7 +1131,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T ); yield* Effect.gen(function* () { - const registry = yield* ProviderRegistry; + const registry = yield* ProviderRegistry.ProviderRegistry; let providers = yield* registry.getProviders; for ( let attempts = 0; @@ -1177,15 +1196,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void)); const providerRegistryLayer = ProviderRegistryLive.pipe( Layer.provideMerge(ProviderInstanceRegistryHydrationLive), - Layer.provideMerge(Layer.succeed(ServerSettingsService, serverSettings)), + Layer.provideMerge( + Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings), + ), Layer.provideMerge( ServerConfig.layerTest(process.cwd(), { prefix: "t3-provider-registry-", }), ), Layer.provideMerge(TestHttpClientLive), - Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)), - Layer.provideMerge(OpenCodeRuntimeLive), + Layer.provideMerge( + Layer.succeed( + ProviderEventLoggers.ProviderEventLoggers, + ProviderEventLoggers.NoOpProviderEventLoggers, + ), + ), + Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive), Layer.updateService(ChildProcessSpawner.ChildProcessSpawner, (spawner) => ChildProcessSpawner.make((command) => { spawnedCommands.push((command as { readonly command: string }).command); @@ -1199,7 +1225,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T ); yield* Effect.gen(function* () { - const registry = yield* ProviderRegistry; + const registry = yield* ProviderRegistry.ProviderRegistry; // Boot-time probe: the default codex instance is enabled with // `firstMissing`, so the real spawner yields ENOENT and the // snapshot should be `status: "error"`. @@ -1291,15 +1317,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void)); const providerRegistryLayer = ProviderRegistryLive.pipe( Layer.provideMerge(ProviderInstanceRegistryHydrationLive), - Layer.provideMerge(Layer.succeed(ServerSettingsService, serverSettings)), + Layer.provideMerge( + Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings), + ), Layer.provideMerge( ServerConfig.layerTest(process.cwd(), { prefix: "t3-provider-registry-", }), ), Layer.provideMerge(TestHttpClientLive), - Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)), - Layer.provideMerge(OpenCodeRuntimeLive), + Layer.provideMerge( + Layer.succeed( + ProviderEventLoggers.ProviderEventLoggers, + ProviderEventLoggers.NoOpProviderEventLoggers, + ), + ), + Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive), Layer.provideMerge(NodeServices.layer), ); const runtimeServices = yield* Layer.build(providerRegistryLayer).pipe( @@ -1307,7 +1340,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T ); yield* Effect.gen(function* () { - const registry = yield* ProviderRegistry; + const registry = yield* ProviderRegistry.ProviderRegistry; const providers = yield* registry.getProviders; const ghost = providers.find((provider) => provider.instanceId === "ghost_main"); @@ -1345,15 +1378,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void)); const providerRegistryLayer = ProviderRegistryLive.pipe( Layer.provideMerge(ProviderInstanceRegistryHydrationLive), - Layer.provideMerge(Layer.succeed(ServerSettingsService, serverSettings)), + Layer.provideMerge( + Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings), + ), Layer.provideMerge( ServerConfig.layerTest(process.cwd(), { prefix: "t3-provider-registry-", }), ), Layer.provideMerge(TestHttpClientLive), - Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)), - Layer.provideMerge(OpenCodeRuntimeLive), + Layer.provideMerge( + Layer.succeed( + ProviderEventLoggers.ProviderEventLoggers, + ProviderEventLoggers.NoOpProviderEventLoggers, + ), + ), + Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive), Layer.provideMerge( mockCommandSpawnerLayer((command, args) => { if (command === "agent") { @@ -1380,13 +1420,13 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T ); const runtimeServices = yield* Layer.build( Layer.mergeAll( - Layer.succeed(ServerSettingsService, serverSettings), + Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings), providerRegistryLayer, ), ).pipe(Scope.provide(scope)); yield* Effect.gen(function* () { - const registry = yield* ProviderRegistry; + const registry = yield* ProviderRegistry.ProviderRegistry; const providers = yield* registry.getProviders; const cursorProvider = providers.find( (provider) => provider.instanceId === ProviderInstanceId.make("cursor"), diff --git a/apps/server/src/provider/providerUpdateSettings.ts b/apps/server/src/provider/providerUpdateSettings.ts index 564af26c78e..308d84a1446 100644 --- a/apps/server/src/provider/providerUpdateSettings.ts +++ b/apps/server/src/provider/providerUpdateSettings.ts @@ -3,7 +3,7 @@ import * as Effect from "effect/Effect"; import * as Equal from "effect/Equal"; import * as Stream from "effect/Stream"; -import type { ServerSettingsShape } from "../serverSettings.ts"; +import type * as ServerSettingsModule from "../serverSettings.ts"; export interface ProviderSnapshotSettings { readonly provider: Settings; @@ -29,7 +29,7 @@ export function haveProviderSnapshotSettingsChanged( export function makeProviderSnapshotSettingsSource( provider: Settings, - serverSettings: ServerSettingsShape, + serverSettings: ServerSettingsModule.ServerSettingsService["Service"], ): { readonly getSettings: Effect.Effect, ServerSettingsError>; readonly streamSettings: Stream.Stream>; diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index 205833289ea..bf4a77743a2 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -69,56 +69,30 @@ import { vi } from "vite-plus/test"; const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z"); -import type { ServerConfigShape } from "./config.ts"; -import { deriveServerPaths, ServerConfig } from "./config.ts"; +import * as ServerConfig from "./config.ts"; import { makeRoutesLayer } from "./server.ts"; -import { - CheckpointDiffQuery, - type CheckpointDiffQueryShape, -} from "./checkpointing/Services/CheckpointDiffQuery.ts"; -import { GitManager, type GitManagerShape } from "./git/GitManager.ts"; -import { Keybindings, type KeybindingsShape } from "./keybindings.ts"; +import * as CheckpointDiffQuery from "./checkpointing/Services/CheckpointDiffQuery.ts"; +import * as GitManager from "./git/GitManager.ts"; +import * as Keybindings from "./keybindings.ts"; import * as ExternalLauncher from "./process/externalLauncher.ts"; -import { - OrchestrationEngineService, - type OrchestrationEngineShape, -} from "./orchestration/Services/OrchestrationEngine.ts"; +import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts"; import { OrchestrationListenerCallbackError } from "./orchestration/Errors.ts"; -import { - ProjectionSnapshotQuery, - type ProjectionSnapshotQueryShape, -} from "./orchestration/Services/ProjectionSnapshotQuery.ts"; +import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts"; import { SqlitePersistenceMemory } from "./persistence/Layers/Sqlite.ts"; import { PersistenceSqlError } from "./persistence/Errors.ts"; -import { - ProviderRegistry, - type ProviderRegistryShape, -} from "./provider/Services/ProviderRegistry.ts"; +import * as ProviderRegistry from "./provider/Services/ProviderRegistry.ts"; import { makeManualOnlyProviderMaintenanceCapabilities } from "./provider/providerMaintenance.ts"; -import { ServerLifecycleEvents, type ServerLifecycleEventsShape } from "./serverLifecycleEvents.ts"; -import { ServerRuntimeStartup, type ServerRuntimeStartupShape } from "./serverRuntimeStartup.ts"; -import { ServerSettingsService, type ServerSettingsShape } from "./serverSettings.ts"; -import { TerminalManager, type TerminalManagerShape } from "./terminal/Services/Manager.ts"; +import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; +import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts"; +import * as ServerSettings from "./serverSettings.ts"; +import * as TerminalManager from "./terminal/Services/Manager.ts"; import * as PreviewManager from "./preview/Manager.ts"; import * as PortScanner from "./preview/PortScanner.ts"; -import { - BrowserTraceCollector, - type BrowserTraceCollectorShape, -} from "./observability/Services/BrowserTraceCollector.ts"; +import * as BrowserTraceCollector from "./observability/Services/BrowserTraceCollector.ts"; import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts"; -import { - ProjectSetupScriptRunner, - ProjectSetupScriptRunnerError, - type ProjectSetupScriptRunnerShape, -} from "./project/Services/ProjectSetupScriptRunner.ts"; -import { - RepositoryIdentityResolver, - type RepositoryIdentityResolverShape, -} from "./project/Services/RepositoryIdentityResolver.ts"; -import { - ServerEnvironment, - type ServerEnvironmentShape, -} from "./environment/Services/ServerEnvironment.ts"; +import * as ProjectSetupScriptRunner from "./project/Services/ProjectSetupScriptRunner.ts"; +import * as RepositoryIdentityResolver from "./project/Services/RepositoryIdentityResolver.ts"; +import * as ServerEnvironment from "./environment/Services/ServerEnvironment.ts"; import * as WorkspaceEntries from "./workspace/WorkspaceEntries.ts"; import { WorkspaceFileSystemLive } from "./workspace/Layers/WorkspaceFileSystem.ts"; import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths.ts"; @@ -132,10 +106,7 @@ import * as ReviewService from "./review/ReviewService.ts"; import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts"; import * as ServerSecretStore from "./auth/ServerSecretStore.ts"; import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; -import { - CloudManagedEndpointRuntime, - type CloudManagedEndpointRuntimeShape, -} from "./cloud/ManagedEndpointRuntime.ts"; +import * as CloudManagedEndpointRuntime from "./cloud/ManagedEndpointRuntime.ts"; import * as CloudCliTokenManager from "./cloud/CliTokenManager.ts"; import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts"; import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts"; @@ -341,32 +312,40 @@ const makeBrowserOtlpPayload = (spanName: string) => }); const buildAppUnderTest = (options?: { - config?: Partial; + config?: Partial; layers?: { - keybindings?: Partial; - providerRegistry?: Partial; - serverSettings?: Partial; - externalLauncher?: Partial; - vcsDriver?: Partial; - vcsDriverRegistry?: Partial; - gitVcsDriver?: Partial; - gitManager?: Partial; - sourceControlRepositoryService?: Partial; - reviewService?: Partial; - vcsStatusBroadcaster?: Partial; - projectSetupScriptRunner?: Partial; - terminalManager?: Partial; - orchestrationEngine?: Partial; - projectionSnapshotQuery?: Partial; - checkpointDiffQuery?: Partial; - browserTraceCollector?: Partial; - serverLifecycleEvents?: Partial; - serverRuntimeStartup?: Partial; - serverEnvironment?: Partial; - repositoryIdentityResolver?: Partial; - cloudManagedEndpointRuntime?: Partial; - relayClient?: Partial; - cloudCliTokenManager?: Partial; + keybindings?: Partial; + providerRegistry?: Partial; + serverSettings?: Partial; + externalLauncher?: Partial; + vcsDriver?: Partial; + vcsDriverRegistry?: Partial; + gitVcsDriver?: Partial; + gitManager?: Partial; + sourceControlRepositoryService?: Partial< + SourceControlRepositoryService.SourceControlRepositoryService["Service"] + >; + reviewService?: Partial; + vcsStatusBroadcaster?: Partial; + projectSetupScriptRunner?: Partial< + ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"] + >; + terminalManager?: Partial; + orchestrationEngine?: Partial; + projectionSnapshotQuery?: Partial; + checkpointDiffQuery?: Partial; + browserTraceCollector?: Partial; + serverLifecycleEvents?: Partial; + serverRuntimeStartup?: Partial; + serverEnvironment?: Partial; + repositoryIdentityResolver?: Partial< + RepositoryIdentityResolver.RepositoryIdentityResolver["Service"] + >; + cloudManagedEndpointRuntime?: Partial< + CloudManagedEndpointRuntime.CloudManagedEndpointRuntime["Service"] + >; + relayClient?: Partial; + cloudCliTokenManager?: Partial; }; }) => Effect.gen(function* () { @@ -374,8 +353,8 @@ const buildAppUnderTest = (options?: { const tempBaseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-router-test-" }); const baseDir = options?.config?.baseDir ?? tempBaseDir; const devUrl = options?.config?.devUrl; - const derivedPaths = yield* deriveServerPaths(baseDir, devUrl); - const config: ServerConfigShape = { + const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, devUrl); + const config: ServerConfig.ServerConfig["Service"] = { logLevel: "Info", traceMinLevel: "Info", traceTimingEnabled: true, @@ -403,8 +382,8 @@ const buildAppUnderTest = (options?: { tailscaleServePort: 443, ...options?.config, }; - const layerConfig = Layer.succeed(ServerConfig, config); - const defaultVcsDriver: VcsDriver.VcsDriverShape = { + const layerConfig = ServerConfig.layer(config); + const defaultVcsDriver: VcsDriver.VcsDriver["Service"] = { capabilities: { kind: "git", supportsWorktrees: true, @@ -502,7 +481,7 @@ const buildAppUnderTest = (options?: { const gitVcsDriverLayer = Layer.mock(GitVcsDriver.GitVcsDriver)({ ...options?.layers?.gitVcsDriver, }); - const gitManagerLayer = Layer.mock(GitManager)({ + const gitManagerLayer = Layer.mock(GitManager.GitManager)({ ...options?.layers?.gitManager, }); const workspaceEntriesLayer = WorkspaceEntries.layer.pipe( @@ -545,7 +524,7 @@ const buildAppUnderTest = (options?: { disableLogger: true, }).pipe( Layer.provide( - Layer.mock(Keybindings)({ + Layer.mock(Keybindings.Keybindings)({ loadConfigState: Effect.succeed({ keybindings: [], issues: [], @@ -555,7 +534,7 @@ const buildAppUnderTest = (options?: { }), ), Layer.provide( - Layer.mock(ProviderRegistry)({ + Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]), refresh: () => Effect.succeed([]), refreshInstance: () => Effect.succeed([]), @@ -569,7 +548,7 @@ const buildAppUnderTest = (options?: { }), ), Layer.provide( - Layer.mock(ServerSettingsService)({ + Layer.mock(ServerSettings.ServerSettingsService)({ start: Effect.void, ready: Effect.void, getSettings: Effect.succeed(DEFAULT_SERVER_SETTINGS), @@ -658,13 +637,13 @@ const buildAppUnderTest = (options?: { ), Layer.provideMerge(vcsStatusBroadcasterLayer), Layer.provide( - Layer.mock(ProjectSetupScriptRunner)({ + Layer.mock(ProjectSetupScriptRunner.ProjectSetupScriptRunner)({ runForThread: () => Effect.succeed({ status: "no-script" as const }), ...options?.layers?.projectSetupScriptRunner, }), ), Layer.provide( - Layer.mock(TerminalManager)({ + Layer.mock(TerminalManager.TerminalManager)({ ...options?.layers?.terminalManager, }), ), @@ -692,7 +671,7 @@ const buildAppUnderTest = (options?: { ), ), Layer.provide( - Layer.mock(OrchestrationEngineService)({ + Layer.mock(OrchestrationEngine.OrchestrationEngineService)({ readEvents: () => Stream.empty, dispatch: () => Effect.succeed({ sequence: 0 }), streamDomainEvents: Stream.empty, @@ -700,7 +679,7 @@ const buildAppUnderTest = (options?: { }), ), Layer.provide( - Layer.mock(ProjectionSnapshotQuery)({ + Layer.mock(ProjectionSnapshotQuery.ProjectionSnapshotQuery)({ getCommandReadModel: () => Effect.succeed(makeDefaultOrchestrationReadModel()), getSnapshot: () => Effect.succeed(makeDefaultOrchestrationReadModel()), getShellSnapshot: () => @@ -729,7 +708,7 @@ const buildAppUnderTest = (options?: { }), ), Layer.provide( - Layer.mock(CheckpointDiffQuery)({ + Layer.mock(CheckpointDiffQuery.CheckpointDiffQuery)({ getTurnDiff: () => Effect.succeed({ threadId: defaultThreadId, @@ -751,13 +730,13 @@ const buildAppUnderTest = (options?: { const appLayer = servedRoutesLayer.pipe( Layer.provide( - Layer.mock(BrowserTraceCollector)({ + Layer.mock(BrowserTraceCollector.BrowserTraceCollector)({ record: () => Effect.void, ...options?.layers?.browserTraceCollector, }), ), Layer.provide( - Layer.mock(ServerLifecycleEvents)({ + Layer.mock(ServerLifecycleEvents.ServerLifecycleEvents)({ publish: (event) => Effect.succeed({ ...(event as any), sequence: 1 }), snapshot: Effect.succeed({ sequence: 0, events: [] }), stream: Stream.empty, @@ -765,7 +744,7 @@ const buildAppUnderTest = (options?: { }), ), Layer.provide( - Layer.mock(ServerRuntimeStartup)({ + Layer.mock(ServerRuntimeStartup.ServerRuntimeStartup)({ awaitCommandReady: Effect.void, markHttpListening: Effect.void, enqueueCommand: (effect) => effect, @@ -773,22 +752,22 @@ const buildAppUnderTest = (options?: { }), ), Layer.provide( - Layer.mock(ServerEnvironment)({ + Layer.mock(ServerEnvironment.ServerEnvironment)({ getEnvironmentId: Effect.succeed(testEnvironmentDescriptor.environmentId), getDescriptor: Effect.succeed(testEnvironmentDescriptor), ...options?.layers?.serverEnvironment, }), ), Layer.provide( - Layer.mock(RepositoryIdentityResolver)({ + Layer.mock(RepositoryIdentityResolver.RepositoryIdentityResolver)({ resolve: () => Effect.succeed(null), ...options?.layers?.repositoryIdentityResolver, }), ), Layer.provide( Layer.succeed( - CloudManagedEndpointRuntime, - CloudManagedEndpointRuntime.of({ + CloudManagedEndpointRuntime.CloudManagedEndpointRuntime, + CloudManagedEndpointRuntime.CloudManagedEndpointRuntime.of({ applyConfig: () => Effect.succeed({ status: "disabled" }), ...options?.layers?.cloudManagedEndpointRuntime, }), @@ -5938,14 +5917,14 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }), ); const fetchRemote = vi.fn( - (_: Parameters[0]) => + (_: Parameters[0]) => Effect.sync(() => { bootstrapGitOperations.push("fetch"); }), ); const fetchedOriginCommit = "0123456789abcdef0123456789abcdef01234567"; const resolveRemoteTrackingCommit = vi.fn( - (_: Parameters[0]) => + (_: Parameters[0]) => Effect.sync(() => { bootstrapGitOperations.push("resolve-remote-commit"); return { @@ -5955,7 +5934,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }), ); const createWorktree = vi.fn( - (_: Parameters[0]) => + (_: Parameters[0]) => Effect.sync(() => { bootstrapGitOperations.push("create-worktree"); return { @@ -5967,7 +5946,11 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }), ); const runForThread = vi.fn( - (_: Parameters[0]) => + ( + _: Parameters< + ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"] + >[0], + ) => Effect.succeed({ status: "started" as const, scriptId: "setup", @@ -6101,7 +6084,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { Effect.gen(function* () { const dispatchedCommands: Array = []; const createWorktree = vi.fn( - (_: Parameters[0]) => + (_: Parameters[0]) => Effect.succeed({ worktree: { refName: "t3code/bootstrap-refName", @@ -6110,8 +6093,16 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }), ); const runForThread = vi.fn( - (_: Parameters[0]) => - Effect.fail(new ProjectSetupScriptRunnerError({ message: "pty unavailable" })), + ( + _: Parameters< + ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"] + >[0], + ) => + Effect.fail( + new ProjectSetupScriptRunner.ProjectSetupScriptRunnerError({ + message: "pty unavailable", + }), + ), ); yield* buildAppUnderTest({ @@ -6195,7 +6186,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { Effect.gen(function* () { const dispatchedCommands: Array = []; const createWorktree = vi.fn( - (_: Parameters[0]) => + (_: Parameters[0]) => Effect.succeed({ worktree: { refName: "t3code/bootstrap-refName", @@ -6204,7 +6195,11 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }), ); const runForThread = vi.fn( - (_: Parameters[0]) => + ( + _: Parameters< + ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"] + >[0], + ) => Effect.succeed({ status: "started" as const, scriptId: "setup", @@ -6314,7 +6309,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { Effect.gen(function* () { const dispatchedCommands: Array = []; const createWorktree = vi.fn( - (_: Parameters[0]) => + (_: Parameters[0]) => Effect.die(new Error("worktree exploded")), ); diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 1da0ea27a65..373dc61bad8 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http"; import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder"; -import { ServerConfig } from "./config.ts"; +import * as ServerConfig from "./config.ts"; import { otlpTracesProxyRouteLayer, assetRouteLayer, @@ -16,15 +16,15 @@ import { fixPath } from "./os-jank.ts"; import { websocketRpcRouteLayer } from "./ws.ts"; import * as ExternalLauncher from "./process/externalLauncher.ts"; import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts"; -import { ServerLifecycleEventsLive } from "./serverLifecycleEvents.ts"; +import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService.ts"; import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory.ts"; import { ProviderSessionRuntimeRepositoryLive } from "./persistence/Layers/ProviderSessionRuntime.ts"; import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry.ts"; -import { ProviderEventLoggersLive } from "./provider/Layers/ProviderEventLoggers.ts"; +import * as ProviderEventLoggers from "./provider/Layers/ProviderEventLoggers.ts"; import { ProviderServiceLive } from "./provider/Layers/ProviderService.ts"; import { ProviderSessionReaperLive } from "./provider/Layers/ProviderSessionReaper.ts"; -import { OpenCodeRuntimeLive } from "./provider/opencodeRuntime.ts"; +import * as OpenCodeRuntime from "./provider/opencodeRuntime.ts"; import { CheckpointDiffQueryLive } from "./checkpointing/Layers/CheckpointDiffQuery.ts"; import { CheckpointStoreLive } from "./checkpointing/Layers/CheckpointStore.ts"; import * as AzureDevOpsCli from "./sourceControl/AzureDevOpsCli.ts"; @@ -40,8 +40,8 @@ import * as PreviewManager from "./preview/Manager.ts"; import * as PortScanner from "./preview/PortScanner.ts"; import * as ProcessRunner from "./processRunner.ts"; import * as GitManager from "./git/GitManager.ts"; -import { KeybindingsLive } from "./keybindings.ts"; -import { ServerRuntimeStartup, ServerRuntimeStartupLive } from "./serverRuntimeStartup.ts"; +import * as Keybindings from "./keybindings.ts"; +import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts"; import { OrchestrationReactorLive } from "./orchestration/Layers/OrchestrationReactor.ts"; import { RuntimeReceiptBusLive } from "./orchestration/Layers/RuntimeReceiptBus.ts"; import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion.ts"; @@ -51,7 +51,7 @@ import { ThreadDeletionReactorLive } from "./orchestration/Layers/ThreadDeletion import * as AgentAwarenessRelay from "./relay/AgentAwarenessRelay.ts"; import { hasCloudPublicConfig } from "./cloud/publicConfig.ts"; import { ProviderRegistryLive } from "./provider/Layers/ProviderRegistry.ts"; -import { ServerSettingsLive } from "./serverSettings.ts"; +import * as ServerSettings from "./serverSettings.ts"; import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts"; import { RepositoryIdentityResolverLive } from "./project/Layers/RepositoryIdentityResolver.ts"; import * as WorkspaceEntries from "./workspace/WorkspaceEntries.ts"; @@ -112,14 +112,14 @@ const PtyAdapterLive = Layer.unwrap( const RelayClientLive = Layer.unwrap( Effect.gen(function* () { - const config = yield* ServerConfig; + const config = yield* ServerConfig.ServerConfig; return RelayClient.layerCloudflared({ baseDir: config.baseDir }); }), ); const HttpServerLive = Layer.unwrap( Effect.gen(function* () { - const config = yield* ServerConfig; + const config = yield* ServerConfig.ServerConfig; if (typeof Bun !== "undefined") { const BunHttpServer = yield* Effect.promise( () => import("@effect/platform-bun/BunHttpServer"), @@ -292,7 +292,7 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe( Layer.provideMerge(ProviderRuntimeLayerLive), Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive)), Layer.provideMerge(PersistenceLayerLive), - Layer.provideMerge(KeybindingsLive), + Layer.provideMerge(Keybindings.layer), Layer.provideMerge(ProviderRegistryLive), // The instance registry is the new routing keystone — text generation, // adapter lookup, and runtime ingestion all resolve `ProviderInstanceId` @@ -305,14 +305,14 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe( // `ProviderService` (canonical stream, written after event normalization). // Provided once at the runtime level so every consumer sees the same // logger instances. - Layer.provideMerge(ProviderEventLoggersLive), + Layer.provideMerge(ProviderEventLoggers.ProviderEventLoggersLive), // `OpenCodeDriver.create()` yields `OpenCodeRuntime`; previously the old // `ProviderRegistryLive` pulled `OpenCodeRuntimeLive` in for itself, but // the rewritten registry reads snapshots off the instance registry and // no longer transitively provides it. Exposing it at the runtime level // keeps a single Live for all opencode consumers. - Layer.provideMerge(OpenCodeRuntimeLive), - Layer.provideMerge(ServerSettingsLive), + Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive), + Layer.provideMerge(ServerSettings.layer.pipe(Layer.provide(ServerSecretStore.layer))), Layer.provideMerge(WorkspaceLayerLive), Layer.provideMerge(ProjectFaviconResolverLayerLive), Layer.provideMerge(RepositoryIdentityResolverLive), @@ -334,11 +334,11 @@ const RuntimeDependenciesLive = RuntimeCoreDependenciesLive.pipe( Layer.provideMerge(TraceDiagnostics.layer), Layer.provideMerge(AnalyticsServiceLayerLive), Layer.provideMerge(ExternalLauncher.layer), - Layer.provideMerge(ServerLifecycleEventsLive), + Layer.provideMerge(ServerLifecycleEvents.layer), Layer.provide(NetService.layer), ); -const RuntimeServicesLive = ServerRuntimeStartupLive.pipe( +const RuntimeServicesLive = ServerRuntimeStartup.layer.pipe( Layer.provideMerge(RuntimeDependenciesLive), ); @@ -361,14 +361,14 @@ export const makeRoutesLayer = Layer.mergeAll( export const makeServerLayer = Layer.unwrap( Effect.gen(function* () { - const config = yield* ServerConfig; + const config = yield* ServerConfig.ServerConfig; yield* fixPath(); const httpListeningLayer = Layer.effectDiscard( Effect.gen(function* () { yield* HttpServer.HttpServer; - const startup = yield* ServerRuntimeStartup; + const startup = yield* ServerRuntimeStartup.ServerRuntimeStartup; yield* startup.markHttpListening; }), ); diff --git a/apps/server/src/serverLifecycleEvents.test.ts b/apps/server/src/serverLifecycleEvents.test.ts index 14fbba9e238..4f7b75fb4bd 100644 --- a/apps/server/src/serverLifecycleEvents.test.ts +++ b/apps/server/src/serverLifecycleEvents.test.ts @@ -4,13 +4,13 @@ import { assertTrue } from "@effect/vitest/utils"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; -import { ServerLifecycleEvents, ServerLifecycleEventsLive } from "./serverLifecycleEvents.ts"; +import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; it.effect( "publishes lifecycle events without subscribers and snapshots the latest welcome/ready", () => Effect.gen(function* () { - const lifecycleEvents = yield* ServerLifecycleEvents; + const lifecycleEvents = yield* ServerLifecycleEvents.ServerLifecycleEvents; const environment = { environmentId: EnvironmentId.make("environment-test"), label: "Test environment", @@ -49,5 +49,5 @@ it.effect( const snapshot = yield* lifecycleEvents.snapshot; assert.equal(snapshot.sequence, 2); assert.deepEqual(snapshot.events.map((event) => event.type).toSorted(), ["ready", "welcome"]); - }).pipe(Effect.provide(ServerLifecycleEventsLive)), + }).pipe(Effect.provide(ServerLifecycleEvents.layer)), ); diff --git a/apps/server/src/serverLifecycleEvents.ts b/apps/server/src/serverLifecycleEvents.ts index 88661b1593a..855d03490ef 100644 --- a/apps/server/src/serverLifecycleEvents.ts +++ b/apps/server/src/serverLifecycleEvents.ts @@ -1,9 +1,9 @@ import type { ServerLifecycleStreamEvent } from "@t3tools/contracts"; +import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as PubSub from "effect/PubSub"; import * as Ref from "effect/Ref"; -import * as Context from "effect/Context"; import * as Stream from "effect/Stream"; type LifecycleEventInput = @@ -15,44 +15,41 @@ interface SnapshotState { readonly events: ReadonlyArray; } -export interface ServerLifecycleEventsShape { - readonly publish: (event: LifecycleEventInput) => Effect.Effect; - readonly snapshot: Effect.Effect; - readonly stream: Stream.Stream; -} - export class ServerLifecycleEvents extends Context.Service< ServerLifecycleEvents, - ServerLifecycleEventsShape + { + readonly publish: (event: LifecycleEventInput) => Effect.Effect; + readonly snapshot: Effect.Effect; + readonly stream: Stream.Stream; + } >()("t3/serverLifecycleEvents") {} -export const ServerLifecycleEventsLive = Layer.effect( - ServerLifecycleEvents, - Effect.gen(function* () { - const pubsub = yield* PubSub.unbounded(); - const state = yield* Ref.make({ - sequence: 0, - events: [], - }); +const make = Effect.gen(function* () { + const pubsub = yield* PubSub.unbounded(); + const state = yield* Ref.make({ + sequence: 0, + events: [], + }); + + return { + publish: (event) => + Ref.modify(state, (current) => { + const nextSequence = current.sequence + 1; + const nextEvent = { + ...event, + sequence: nextSequence, + } satisfies ServerLifecycleStreamEvent; + const nextEvents = + nextEvent.type === "welcome" + ? [nextEvent, ...current.events.filter((entry) => entry.type !== "welcome")] + : [nextEvent, ...current.events.filter((entry) => entry.type !== "ready")]; + return [nextEvent, { sequence: nextSequence, events: nextEvents }] as const; + }).pipe(Effect.tap((event) => PubSub.publish(pubsub, event))), + snapshot: Ref.get(state), + get stream() { + return Stream.fromPubSub(pubsub); + }, + } satisfies ServerLifecycleEvents["Service"]; +}); - return { - publish: (event) => - Ref.modify(state, (current) => { - const nextSequence = current.sequence + 1; - const nextEvent = { - ...event, - sequence: nextSequence, - } satisfies ServerLifecycleStreamEvent; - const nextEvents = - nextEvent.type === "welcome" - ? [nextEvent, ...current.events.filter((entry) => entry.type !== "welcome")] - : [nextEvent, ...current.events.filter((entry) => entry.type !== "ready")]; - return [nextEvent, { sequence: nextSequence, events: nextEvents }] as const; - }).pipe(Effect.tap((event) => PubSub.publish(pubsub, event))), - snapshot: Ref.get(state), - get stream() { - return Stream.fromPubSub(pubsub); - }, - } satisfies ServerLifecycleEventsShape; - }), -); +export const layer = Layer.effect(ServerLifecycleEvents, make); diff --git a/apps/server/src/serverRuntimeStartup.test.ts b/apps/server/src/serverRuntimeStartup.test.ts index 90eebe33820..a11beba794d 100644 --- a/apps/server/src/serverRuntimeStartup.test.ts +++ b/apps/server/src/serverRuntimeStartup.test.ts @@ -10,24 +10,14 @@ import * as PlatformError from "effect/PlatformError"; import * as Ref from "effect/Ref"; import * as Stream from "effect/Stream"; -import { ServerConfig } from "./config.ts"; -import { - OrchestrationEngineService, - type OrchestrationEngineShape, -} from "./orchestration/Services/OrchestrationEngine.ts"; -import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts"; -import { AnalyticsService } from "./telemetry/Services/AnalyticsService.ts"; -import { - getAutoBootstrapDefaultModelSelection, - launchStartupHeartbeat, - makeCommandGate, - resolveAutoBootstrapWelcomeTargets, - resolveWelcomeBase, - ServerRuntimeStartupError, -} from "./serverRuntimeStartup.ts"; +import * as ServerConfig from "./config.ts"; +import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts"; +import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts"; +import * as AnalyticsService from "./telemetry/Services/AnalyticsService.ts"; +import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts"; it("uses the canonical Codex default for auto-bootstrapped model selection", () => { - assert.deepStrictEqual(getAutoBootstrapDefaultModelSelection(), { + assert.deepStrictEqual(ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(), { instanceId: ProviderInstanceId.make("codex"), model: DEFAULT_MODEL, }); @@ -37,7 +27,7 @@ it.effect("enqueueCommand waits for readiness and then drains queued work", () = Effect.scoped( Effect.gen(function* () { const executionCount = yield* Ref.make(0); - const commandGate = yield* makeCommandGate; + const commandGate = yield* ServerRuntimeStartup.makeCommandGate; const queuedCommandFiber = yield* commandGate .enqueueCommand(Ref.updateAndGet(executionCount, (count) => count + 1)) @@ -58,7 +48,7 @@ it.effect("enqueueCommand waits for readiness and then drains queued work", () = it.effect("enqueueCommand fails queued work when readiness fails", () => Effect.scoped( Effect.gen(function* () { - const commandGate = yield* makeCommandGate; + const commandGate = yield* ServerRuntimeStartup.makeCommandGate; const failure = yield* Deferred.make(); const queuedCommandFiber = yield* commandGate @@ -66,13 +56,13 @@ it.effect("enqueueCommand fails queued work when readiness fails", () => .pipe(Effect.forkScoped); yield* commandGate.failCommandReady( - new ServerRuntimeStartupError({ - message: "startup failed", + new ServerRuntimeStartup.ServerRuntimeStartupError({ + stage: "command-readiness", }), ); const error = yield* Effect.flip(Fiber.join(queuedCommandFiber)); - assert.equal(error.message, "startup failed"); + assert.equal(error.message, "Server runtime startup failed before command readiness."); }), ), ); @@ -82,8 +72,8 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa Effect.gen(function* () { const releaseCounts = yield* Deferred.make(); - yield* launchStartupHeartbeat.pipe( - Effect.provideService(ProjectionSnapshotQuery, { + yield* ServerRuntimeStartup.launchStartupHeartbeat.pipe( + Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { getCommandReadModel: () => Effect.die("unused"), getSnapshot: () => Effect.die("unused"), getShellSnapshot: () => Effect.die("unused"), @@ -104,7 +94,7 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa getThreadShellById: () => Effect.succeed(Option.none()), getThreadDetailById: () => Effect.succeed(Option.none()), }), - Effect.provideService(AnalyticsService, { + Effect.provideService(AnalyticsService.AnalyticsService, { record: () => Effect.void, flush: Effect.void, }), @@ -115,8 +105,8 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa it.effect("resolveWelcomeBase derives cwd and project name from server config", () => Effect.gen(function* () { - const welcome = yield* resolveWelcomeBase.pipe( - Effect.provideService(ServerConfig, { + const welcome = yield* ServerRuntimeStartup.resolveWelcomeBase.pipe( + Effect.provideService(ServerConfig.ServerConfig, { cwd: "/tmp/startup-project", } as never), ); @@ -134,12 +124,12 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa return Effect.gen(function* () { const dispatchCalls = yield* Ref.make>([]); - const targets = yield* resolveAutoBootstrapWelcomeTargets.pipe( - Effect.provideService(ServerConfig, { + const targets = yield* ServerRuntimeStartup.resolveAutoBootstrapWelcomeTargets.pipe( + Effect.provideService(ServerConfig.ServerConfig, { cwd: "/tmp/startup-project", autoBootstrapProjectFromCwd: true, } as never), - Effect.provideService(ProjectionSnapshotQuery, { + Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { getCommandReadModel: () => Effect.die("unused"), getSnapshot: () => Effect.die("unused"), getShellSnapshot: () => Effect.die("unused"), @@ -152,7 +142,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa id: bootstrapProjectId, title: "Startup Project", workspaceRoot: "/tmp/startup-project", - defaultModelSelection: getAutoBootstrapDefaultModelSelection(), + defaultModelSelection: ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(), scripts: [], createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", @@ -166,14 +156,14 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa getThreadShellById: () => Effect.die("unused"), getThreadDetailById: () => Effect.die("unused"), }), - Effect.provideService(OrchestrationEngineService, { + Effect.provideService(OrchestrationEngine.OrchestrationEngineService, { readEvents: () => Stream.empty, dispatch: (command) => Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe( Effect.as({ sequence: 1 }), ), streamDomainEvents: Stream.empty, - } satisfies OrchestrationEngineShape), + } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]), Effect.provide(NodeServices.layer), ); @@ -188,12 +178,12 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa it.effect("resolveAutoBootstrapWelcomeTargets creates a project and thread when missing", () => Effect.gen(function* () { const dispatchCalls = yield* Ref.make>([]); - const targets = yield* resolveAutoBootstrapWelcomeTargets.pipe( - Effect.provideService(ServerConfig, { + const targets = yield* ServerRuntimeStartup.resolveAutoBootstrapWelcomeTargets.pipe( + Effect.provideService(ServerConfig.ServerConfig, { cwd: "/tmp/startup-project", autoBootstrapProjectFromCwd: true, } as never), - Effect.provideService(ProjectionSnapshotQuery, { + Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { getCommandReadModel: () => Effect.die("unused"), getSnapshot: () => Effect.die("unused"), getShellSnapshot: () => Effect.die("unused"), @@ -208,14 +198,14 @@ it.effect("resolveAutoBootstrapWelcomeTargets creates a project and thread when getThreadShellById: () => Effect.die("unused"), getThreadDetailById: () => Effect.die("unused"), }), - Effect.provideService(OrchestrationEngineService, { + Effect.provideService(OrchestrationEngine.OrchestrationEngineService, { readEvents: () => Stream.empty, dispatch: (command) => Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe( Effect.as({ sequence: 1 }), ), streamDomainEvents: Stream.empty, - } satisfies OrchestrationEngineShape), + } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]), Effect.provide(NodeServices.layer), ); @@ -236,12 +226,12 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa }); const dispatchCalls = yield* Ref.make>([]); - const error = yield* resolveAutoBootstrapWelcomeTargets.pipe( - Effect.provideService(ServerConfig, { + const error = yield* ServerRuntimeStartup.resolveAutoBootstrapWelcomeTargets.pipe( + Effect.provideService(ServerConfig.ServerConfig, { cwd: "/tmp/startup-project", autoBootstrapProjectFromCwd: true, } as never), - Effect.provideService(ProjectionSnapshotQuery, { + Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { getCommandReadModel: () => Effect.die("unused"), getSnapshot: () => Effect.die("unused"), getShellSnapshot: () => Effect.die("unused"), @@ -256,14 +246,14 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa getThreadShellById: () => Effect.die("unused"), getThreadDetailById: () => Effect.die("unused"), }), - Effect.provideService(OrchestrationEngineService, { + Effect.provideService(OrchestrationEngine.OrchestrationEngineService, { readEvents: () => Stream.empty, dispatch: (command) => Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe( Effect.as({ sequence: 1 }), ), streamDomainEvents: Stream.empty, - } satisfies OrchestrationEngineShape), + } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]), Effect.provideService(Crypto.Crypto, { ...crypto, randomUUIDv4: Effect.fail(uuidError), diff --git a/apps/server/src/serverRuntimeStartup.ts b/apps/server/src/serverRuntimeStartup.ts index cde308ffe42..dab6143e11c 100644 --- a/apps/server/src/serverRuntimeStartup.ts +++ b/apps/server/src/serverRuntimeStartup.ts @@ -7,8 +7,10 @@ import { ProviderInstanceId, ThreadId, } from "@t3tools/contracts"; -import * as Data from "effect/Data"; +import * as Console from "effect/Console"; +import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; +import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; @@ -17,23 +19,21 @@ import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; +import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; -import * as Context from "effect/Context"; -import * as Console from "effect/Console"; -import * as DateTime from "effect/DateTime"; -import { ServerConfig } from "./config.ts"; -import { Keybindings } from "./keybindings.ts"; +import * as ServerConfig from "./config.ts"; +import * as Keybindings from "./keybindings.ts"; import * as ExternalLauncher from "./process/externalLauncher.ts"; -import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine.ts"; -import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts"; -import { OrchestrationReactor } from "./orchestration/Services/OrchestrationReactor.ts"; -import { ServerLifecycleEvents } from "./serverLifecycleEvents.ts"; -import { ServerSettingsService } from "./serverSettings.ts"; -import { ServerEnvironment } from "./environment/Services/ServerEnvironment.ts"; -import { AnalyticsService } from "./telemetry/Services/AnalyticsService.ts"; +import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts"; +import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts"; +import * as OrchestrationReactor from "./orchestration/Services/OrchestrationReactor.ts"; +import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; +import * as ServerSettings from "./serverSettings.ts"; +import * as ServerEnvironment from "./environment/Services/ServerEnvironment.ts"; +import * as AnalyticsService from "./telemetry/Services/AnalyticsService.ts"; import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; -import { ProviderSessionReaper } from "./provider/Services/ProviderSessionReaper.ts"; +import * as ProviderSessionReaper from "./provider/Services/ProviderSessionReaper.ts"; import { formatHeadlessServeOutput, formatHostForUrl, @@ -41,22 +41,30 @@ import { issueHeadlessServeAccessInfo, } from "./startupAccess.ts"; -export class ServerRuntimeStartupError extends Data.TaggedError("ServerRuntimeStartupError")<{ - readonly message: string; - readonly cause?: unknown; -}> {} - -export interface ServerRuntimeStartupShape { - readonly awaitCommandReady: Effect.Effect; - readonly markHttpListening: Effect.Effect; - readonly enqueueCommand: ( - effect: Effect.Effect, - ) => Effect.Effect; +export class ServerRuntimeStartupError extends Schema.TaggedErrorClass()( + "ServerRuntimeStartupError", + { + stage: Schema.Literal("command-readiness"), + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + switch (this.stage) { + case "command-readiness": + return "Server runtime startup failed before command readiness."; + } + } } export class ServerRuntimeStartup extends Context.Service< ServerRuntimeStartup, - ServerRuntimeStartupShape + { + readonly awaitCommandReady: Effect.Effect; + readonly markHttpListening: Effect.Effect; + readonly enqueueCommand: ( + effect: Effect.Effect, + ) => Effect.Effect; + } >()("t3/serverRuntimeStartup") {} interface QueuedCommand { @@ -124,8 +132,8 @@ export const makeCommandGate = Effect.gen(function* () { }); export const recordStartupHeartbeat = Effect.gen(function* () { - const analytics = yield* AnalyticsService; - const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; + const analytics = yield* AnalyticsService.AnalyticsService; + const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; const { threadCount, projectCount } = yield* projectionSnapshotQuery.getCounts().pipe( Effect.catch((cause) => @@ -160,7 +168,7 @@ export const getAutoBootstrapDefaultModelSelection = (): ModelSelection => ({ }); export const resolveWelcomeBase = Effect.gen(function* () { - const serverConfig = yield* ServerConfig; + const serverConfig = yield* ServerConfig.ServerConfig; const segments = serverConfig.cwd.split(/[/\\]/).filter(Boolean); const projectName = segments[segments.length - 1] ?? "project"; @@ -173,9 +181,9 @@ export const resolveWelcomeBase = Effect.gen(function* () { export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () { const crypto = yield* Crypto.Crypto; const randomUUID = crypto.randomUUIDv4; - const serverConfig = yield* ServerConfig; - const projectionReadModelQuery = yield* ProjectionSnapshotQuery; - const orchestrationEngine = yield* OrchestrationEngineService; + const serverConfig = yield* ServerConfig.ServerConfig; + const projectionReadModelQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; + const orchestrationEngine = yield* OrchestrationEngine.OrchestrationEngineService; const path = yield* Path.Path; let bootstrapProjectId: ProjectId | undefined; @@ -243,7 +251,7 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () { }); const resolveStartupBrowserTarget = Effect.gen(function* () { - const serverConfig = yield* ServerConfig; + const serverConfig = yield* ServerConfig.ServerConfig; const serverAuth = yield* EnvironmentAuth.EnvironmentAuth; const localUrl = `http://localhost:${serverConfig.port}`; const bindUrl = @@ -260,7 +268,7 @@ const resolveStartupBrowserTarget = Effect.gen(function* () { const maybeOpenBrowser = (target: string) => Effect.gen(function* () { - const serverConfig = yield* ServerConfig; + const serverConfig = yield* ServerConfig.ServerConfig; if (serverConfig.noBrowser) { return; } @@ -281,14 +289,14 @@ const runStartupPhase = (phase: string, effect: Effect.Effect) Effect.withSpan(`server.startup.${phase}`), ); -export const makeServerRuntimeStartup = Effect.gen(function* () { - const serverConfig = yield* ServerConfig; - const keybindings = yield* Keybindings; - const orchestrationReactor = yield* OrchestrationReactor; - const providerSessionReaper = yield* ProviderSessionReaper; - const lifecycleEvents = yield* ServerLifecycleEvents; - const serverSettings = yield* ServerSettingsService; - const serverEnvironment = yield* ServerEnvironment; +const make = Effect.gen(function* () { + const serverConfig = yield* ServerConfig.ServerConfig; + const keybindings = yield* Keybindings.Keybindings; + const orchestrationReactor = yield* OrchestrationReactor.OrchestrationReactor; + const providerSessionReaper = yield* ProviderSessionReaper.ProviderSessionReaper; + const lifecycleEvents = yield* ServerLifecycleEvents.ServerLifecycleEvents; + const serverSettings = yield* ServerSettings.ServerSettingsService; + const serverEnvironment = yield* ServerEnvironment.ServerEnvironment; const crypto = yield* Crypto.Crypto; const commandGate = yield* makeCommandGate; @@ -409,7 +417,7 @@ export const makeServerRuntimeStartup = Effect.gen(function* () { const startupExit = yield* Effect.exit(startup); if (Exit.isFailure(startupExit)) { const error = new ServerRuntimeStartupError({ - message: "Server runtime startup failed before command readiness.", + stage: "command-readiness", cause: startupExit.cause, }); yield* Effect.logError("server runtime startup failed", { cause: startupExit.cause }); @@ -461,10 +469,7 @@ export const makeServerRuntimeStartup = Effect.gen(function* () { awaitCommandReady: commandGate.awaitCommandReady, markHttpListening: Deferred.succeed(httpListening, undefined), enqueueCommand: commandGate.enqueueCommand, - } satisfies ServerRuntimeStartupShape; + } satisfies ServerRuntimeStartup["Service"]; }); -export const ServerRuntimeStartupLive = Layer.effect( - ServerRuntimeStartup, - makeServerRuntimeStartup, -); +export const layer = Layer.effect(ServerRuntimeStartup, make); diff --git a/apps/server/src/serverRuntimeState.ts b/apps/server/src/serverRuntimeState.ts index 996f9a2bfc9..289bddcb8bb 100644 --- a/apps/server/src/serverRuntimeState.ts +++ b/apps/server/src/serverRuntimeState.ts @@ -5,7 +5,7 @@ import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import { writeFileStringAtomically } from "./atomicWrite.ts"; -import { type ServerConfigShape } from "./config.ts"; +import type * as ServerConfig from "./config.ts"; import { formatHostForUrl, isWildcardHost } from "./startupAccess.ts"; export const PersistedServerRuntimeState = Schema.Struct({ @@ -23,7 +23,7 @@ const decodePersistedServerRuntimeState = Schema.decodeUnknownEffect( ); const runtimeOriginForConfig = ( - config: Pick, + config: Pick, port: number, ): PersistedServerRuntimeState["origin"] => { const hostname = @@ -32,7 +32,7 @@ const runtimeOriginForConfig = ( }; export const makePersistedServerRuntimeState = (input: { - readonly config: Pick; + readonly config: Pick; readonly port: number; }): Effect.Effect => Effect.map(DateTime.now, (now) => ({ diff --git a/apps/server/src/serverSettings.test.ts b/apps/server/src/serverSettings.test.ts index d24f2ee2826..87feee669ec 100644 --- a/apps/server/src/serverSettings.test.ts +++ b/apps/server/src/serverSettings.test.ts @@ -13,14 +13,16 @@ import * as Duration from "effect/Duration"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; -import { ServerConfig } from "./config.ts"; -import { ServerSettingsLive, ServerSettingsService } from "./serverSettings.ts"; +import * as ServerSecretStore from "./auth/ServerSecretStore.ts"; +import * as ServerConfig from "./config.ts"; +import * as ServerSettingsModule from "./serverSettings.ts"; const decodeSettingsPatch = Schema.decodeUnknownEffect(ServerSettingsPatch); const decodeServerSettings = Schema.decodeUnknownEffect(ServerSettings); const makeServerSettingsLayer = () => - ServerSettingsLive.pipe( + ServerSettingsModule.layer.pipe( + Layer.provide(ServerSecretStore.layer), Layer.provideMerge( Layer.fresh( ServerConfig.layerTest(process.cwd(), { @@ -77,7 +79,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { it.effect("deep merges nested settings updates without dropping siblings", () => Effect.gen(function* () { - const serverSettings = yield* ServerSettingsService; + const serverSettings = yield* ServerSettingsModule.ServerSettingsService; yield* serverSettings.updateSettings({ providers: { @@ -145,7 +147,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { it.effect("preserves model when switching providers via textGenerationModelSelection", () => Effect.gen(function* () { - const serverSettings = yield* ServerSettingsService; + const serverSettings = yield* ServerSettingsModule.ServerSettingsService; // Start with Claude text generation selection yield* serverSettings.updateSettings({ @@ -183,7 +185,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { it.effect("preserves custom provider instance text generation selections", () => Effect.gen(function* () { - const serverSettings = yield* ServerSettingsService; + const serverSettings = yield* ServerSettingsModule.ServerSettingsService; const next = yield* serverSettings.updateSettings({ providerInstances: { @@ -210,7 +212,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { "uses explicit provider instance enabled state over legacy provider enabled state", () => Effect.gen(function* () { - const serverSettings = yield* ServerSettingsService; + const serverSettings = yield* ServerSettingsModule.ServerSettingsService; const instanceId = ProviderInstanceId.make("claude_openrouter"); const next = yield* serverSettings.updateSettings({ @@ -241,7 +243,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { it.effect("preserves enabled text generation selections for non-built-in drivers", () => Effect.gen(function* () { - const serverSettings = yield* ServerSettingsService; + const serverSettings = yield* ServerSettingsModule.ServerSettingsService; const instanceId = ProviderInstanceId.make("openrouter_text"); const next = yield* serverSettings.updateSettings({ @@ -267,7 +269,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { it.effect("drops stale text generation options when resetting model selection", () => Effect.gen(function* () { - const serverSettings = yield* ServerSettingsService; + const serverSettings = yield* ServerSettingsModule.ServerSettingsService; yield* serverSettings.updateSettings({ textGenerationModelSelection: { @@ -300,7 +302,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { it.effect("replaces provider instance maps when clearing optional fields", () => Effect.gen(function* () { - const serverSettings = yield* ServerSettingsService; + const serverSettings = yield* ServerSettingsModule.ServerSettingsService; const codexId = ProviderInstanceId.make("codex"); yield* serverSettings.updateSettings({ @@ -337,7 +339,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { it.effect("trims provider path settings when updates are applied", () => Effect.gen(function* () { - const serverSettings = yield* ServerSettingsService; + const serverSettings = yield* ServerSettingsModule.ServerSettingsService; const next = yield* serverSettings.updateSettings({ providers: { @@ -382,7 +384,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { it.effect("trims observability settings when updates are applied", () => Effect.gen(function* () { - const serverSettings = yield* ServerSettingsService; + const serverSettings = yield* ServerSettingsModule.ServerSettingsService; const next = yield* serverSettings.updateSettings({ addProjectBaseDirectory: " ~/Development ", @@ -402,7 +404,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { it.effect("defaults blank binary paths to provider executables", () => Effect.gen(function* () { - const serverSettings = yield* ServerSettingsService; + const serverSettings = yield* ServerSettingsModule.ServerSettingsService; const next = yield* serverSettings.updateSettings({ providers: { @@ -422,8 +424,8 @@ it.layer(NodeServices.layer)("server settings", (it) => { it.effect("writes only non-default server settings to disk", () => Effect.gen(function* () { - const serverSettings = yield* ServerSettingsService; - const serverConfig = yield* ServerConfig; + const serverSettings = yield* ServerSettingsModule.ServerSettingsService; + const serverConfig = yield* ServerConfig.ServerConfig; const fileSystem = yield* FileSystem.FileSystem; const next = yield* serverSettings.updateSettings({ addProjectBaseDirectory: "~/Development", @@ -469,8 +471,8 @@ it.layer(NodeServices.layer)("server settings", (it) => { it.effect("stores sensitive provider instance environment values outside settings.json", () => Effect.gen(function* () { - const serverSettings = yield* ServerSettingsService; - const serverConfig = yield* ServerConfig; + const serverSettings = yield* ServerSettingsModule.ServerSettingsService; + const serverConfig = yield* ServerConfig.ServerConfig; const fileSystem = yield* FileSystem.FileSystem; const instanceId = ProviderInstanceId.make("codex_personal"); diff --git a/apps/server/src/serverSettings.ts b/apps/server/src/serverSettings.ts index 6e1ceb16a8d..a5fcdc30c02 100644 --- a/apps/server/src/serverSettings.ts +++ b/apps/server/src/serverSettings.ts @@ -26,26 +26,26 @@ import { type ServerSettingsPatch, } from "@t3tools/contracts"; import * as Cache from "effect/Cache"; +import * as Cause from "effect/Cause"; +import * as Context from "effect/Context"; import * as Deferred from "effect/Deferred"; import * as Duration from "effect/Duration"; +import * as Equal from "effect/Equal"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; -import * as Equal from "effect/Equal"; import * as PubSub from "effect/PubSub"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as SchemaIssue from "effect/SchemaIssue"; +import * as Semaphore from "effect/Semaphore"; import * as Scope from "effect/Scope"; -import * as Context from "effect/Context"; import * as Stream from "effect/Stream"; -import * as Cause from "effect/Cause"; -import * as Semaphore from "effect/Semaphore"; import { writeFileStringAtomically } from "./atomicWrite.ts"; -import { ServerConfig } from "./config.ts"; +import * as ServerConfig from "./config.ts"; import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct"; import { fromJsonStringPretty, fromLenientJson } from "@t3tools/shared/schemaJson"; import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings"; @@ -109,59 +109,60 @@ export function redactServerSettingsForClient(settings: ServerSettings): ServerS return { ...settings, providerInstances }; } -export interface ServerSettingsShape { - /** Start the settings runtime and attach file watching. */ - readonly start: Effect.Effect; - - /** Await settings runtime readiness. */ - readonly ready: Effect.Effect; +export class ServerSettingsService extends Context.Service< + ServerSettingsService, + { + /** Start the settings runtime and attach file watching. */ + readonly start: Effect.Effect; - /** Read the current settings. */ - readonly getSettings: Effect.Effect; + /** Await settings runtime readiness. */ + readonly ready: Effect.Effect; - /** Patch settings and persist. Returns the new full settings object. */ - readonly updateSettings: ( - patch: ServerSettingsPatch, - ) => Effect.Effect; + /** Read the current settings. */ + readonly getSettings: Effect.Effect; - /** Stream of settings change events. */ - readonly streamChanges: Stream.Stream; -} + /** Patch settings and persist. Returns the new full settings object. */ + readonly updateSettings: ( + patch: ServerSettingsPatch, + ) => Effect.Effect; -export class ServerSettingsService extends Context.Service< - ServerSettingsService, - ServerSettingsShape + /** Stream of settings change events. */ + readonly streamChanges: Stream.Stream; + } >()("t3/serverSettings/ServerSettingsService") { - static readonly layerTest = (overrides: DeepPartial = {}) => - Layer.effect( - ServerSettingsService, - Effect.gen(function* () { - const { automaticGitFetchInterval, ...overridesForMerge } = overrides; - const merged = deepMerge(DEFAULT_SERVER_SETTINGS, overridesForMerge); - const initialSettings = yield* normalizeServerSettings({ - ...merged, - ...(automaticGitFetchInterval !== undefined - ? { automaticGitFetchInterval: automaticGitFetchInterval as Duration.Duration } - : {}), - }); - const currentSettingsRef = yield* Ref.make(initialSettings); - - return { - start: Effect.void, - ready: Effect.void, - getSettings: Ref.get(currentSettingsRef), - updateSettings: (patch) => - Ref.get(currentSettingsRef).pipe( - Effect.map((currentSettings) => applyServerSettingsPatch(currentSettings, patch)), - Effect.flatMap(normalizeServerSettings), - Effect.tap((nextSettings) => Ref.set(currentSettingsRef, nextSettings)), - ), - streamChanges: Stream.empty, - } satisfies ServerSettingsShape; - }), - ); + /** @deprecated Import and use `layerTest` from this module. */ + static readonly layerTest = (overrides: DeepPartial = {}) => layerTest(overrides); } +const makeTest = (overrides: DeepPartial = {}) => + Effect.gen(function* () { + const { automaticGitFetchInterval, ...overridesForMerge } = overrides; + const merged = deepMerge(DEFAULT_SERVER_SETTINGS, overridesForMerge); + const initialSettings = yield* normalizeServerSettings({ + ...merged, + ...(automaticGitFetchInterval !== undefined + ? { automaticGitFetchInterval: automaticGitFetchInterval as Duration.Duration } + : {}), + }); + const currentSettingsRef = yield* Ref.make(initialSettings); + + return { + start: Effect.void, + ready: Effect.void, + getSettings: Ref.get(currentSettingsRef), + updateSettings: (patch) => + Ref.get(currentSettingsRef).pipe( + Effect.map((currentSettings) => applyServerSettingsPatch(currentSettings, patch)), + Effect.flatMap(normalizeServerSettings), + Effect.tap((nextSettings) => Ref.set(currentSettingsRef, nextSettings)), + ), + streamChanges: Stream.empty, + } satisfies ServerSettingsService["Service"]; + }); + +export const layerTest = (overrides: DeepPartial = {}) => + Layer.effect(ServerSettingsService, makeTest(overrides)); + const ServerSettingsJson = fromLenientJson(ServerSettings); const decodeServerSettingsJsonExit = Schema.decodeUnknownExit(ServerSettingsJson); @@ -255,8 +256,8 @@ function stripDefaultServerSettings(current: unknown, defaults: unknown): unknow return Object.is(current, defaults) ? undefined : current; } -const makeServerSettings = Effect.gen(function* () { - const { settingsPath } = yield* ServerConfig; +const make = Effect.gen(function* () { + const { settingsPath } = yield* ServerConfig.ServerConfig; const fs = yield* FileSystem.FileSystem; const pathService = yield* Path.Path; const secretStore = yield* ServerSecretStore.ServerSecretStore; @@ -578,9 +579,7 @@ const makeServerSettings = Effect.gen(function* () { Stream.map(resolveTextGenerationProvider), ); }, - } satisfies ServerSettingsShape; + } satisfies ServerSettingsService["Service"]; }); -export const ServerSettingsLive = Layer.effect(ServerSettingsService, makeServerSettings).pipe( - Layer.provide(ServerSecretStore.layer), -); +export const layer = Layer.effect(ServerSettingsService, make);