From 91a6646efd98622dc9cef7061387e20bc389463f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 12:14:10 -0700 Subject: [PATCH 01/39] chore(deps): upgrade Effect to 4.0.2 (#17571) Co-authored-by: Claude Opus 5.5 --- .../SqlStatementCounter.integration.ts | 11 +- .../TransferBudgetReport.integration.ts | 6 +- apps/server/src/cli/trace.test.ts | 11 +- apps/server/src/cli/trace.ts | 13 +- apps/server/src/http.test.ts | 2 +- apps/server/src/mcp/McpDeviceToolkit.test.ts | 3 + apps/server/src/mcp/McpHttpServer.test.ts | 45 +- apps/server/src/mcp/McpHttpServer.ts | 24 +- ...OrchestratorMcpToolkit.integration.test.ts | 2 + apps/server/src/mcp/toolkits/core.test.ts | 19 +- .../mcp/toolkits/environment/handlers.test.ts | 67 ++ .../toolkits/worktree/registration.test.ts | 14 + .../observability/RpcInstrumentation.test.ts | 2 +- .../FoundationPersistence.test.ts | 7 +- docs/operations/relay-observability.md | 2 +- infra/relay/src/hooks/HookForwarder.test.ts | 4 +- infra/relay/src/hooks/HookForwarder.ts | 3 +- infra/relay/src/http/Api.test.ts | 7 +- .../src/connection/outdatedHostUpdate.ts | 3 +- packages/client-runtime/src/rpc/protocol.ts | 4 + .../client-runtime/src/rpc/session.test.ts | 2 +- packages/client-runtime/src/rpc/session.ts | 5 +- packages/shared/src/otelEnvironment.test.ts | 5 +- ....0.1.patch => @effect__vitest@4.0.2.patch} | 32 +- patches/effect@4.0.1.patch | 139 ---- patches/effect@4.0.2.patch | 31 + pnpm-lock.yaml | 655 +++++++++--------- pnpm-workspace.yaml | 38 +- 28 files changed, 608 insertions(+), 548 deletions(-) rename patches/{@effect__vitest@4.0.1.patch => @effect__vitest@4.0.2.patch} (84%) delete mode 100644 patches/effect@4.0.1.patch create mode 100644 patches/effect@4.0.2.patch diff --git a/apps/server/integration/SqlStatementCounter.integration.ts b/apps/server/integration/SqlStatementCounter.integration.ts index e3a3aac3fba0..c75fd5d9b73f 100644 --- a/apps/server/integration/SqlStatementCounter.integration.ts +++ b/apps/server/integration/SqlStatementCounter.integration.ts @@ -7,16 +7,17 @@ export interface SqlStatementCounter { } /** - * Counts `sql.execute` spans, which the Effect SQL client opens once per - * statement. Spans behave exactly as with the native tracer. Install the same - * counter on every runtime under test, otherwise statements run by background - * reactors and statements run by request handlers land in different counters. + * Counts the client spans the Effect SQL client opens once per statement. Our + * SQLite client names them after its `db.system.name`, `sqlite`. Spans behave + * exactly as with the native tracer. Install the same counter on every runtime + * under test, otherwise statements run by background reactors and statements + * run by request handlers land in different counters. */ export function makeSqlStatementCounter(): SqlStatementCounter { let statements = 0; const tracer = Tracer.make({ span: (options) => { - if (options.name === "sql.execute") statements += 1; + if (options.kind === "client" && options.name === "sqlite") statements += 1; return new Tracer.NativeSpan(options); }, }); diff --git a/apps/server/integration/TransferBudgetReport.integration.ts b/apps/server/integration/TransferBudgetReport.integration.ts index 53f46d576d2d..de513dbe0f0c 100644 --- a/apps/server/integration/TransferBudgetReport.integration.ts +++ b/apps/server/integration/TransferBudgetReport.integration.ts @@ -30,9 +30,9 @@ export interface TransferBudgetRun { /** The second client resubscribes after the turn from the cursor it held before it. */ readonly reconnectThread: WebSocketCatchUpMeasurement; readonly reconnectShell: WebSocketCatchUpMeasurement; - /** `sql.execute` spans opened server-wide during the measured turn. */ + /** SQL statement spans opened server-wide during the measured turn. */ readonly measuredTurnSqlStatements: number; - /** `sql.execute` spans opened while serving both reconnect catch-ups. */ + /** SQL statement spans opened while serving both reconnect catch-ups. */ readonly reconnectSqlStatements: number; } @@ -200,7 +200,7 @@ export function formatTransferBudgetReport(runs: ReadonlyArray { expect(spanNames).toEqual([]); yield* client.get("/api/environment"); - expect(spanNames).toContain("http.server GET"); + expect(spanNames).toEqual(["GET"]); }).pipe( Effect.scoped, Effect.provideService( diff --git a/apps/server/src/mcp/McpDeviceToolkit.test.ts b/apps/server/src/mcp/McpDeviceToolkit.test.ts index a77e8f600252..e1a688ff64f4 100644 --- a/apps/server/src/mcp/McpDeviceToolkit.test.ts +++ b/apps/server/src/mcp/McpDeviceToolkit.test.ts @@ -197,6 +197,9 @@ it.effect("rejects unavailable agent access before booting or opening a device", Layer.provideMerge(McpServer.McpServer.layer), Layer.provideMerge(McpToolAccessTestkit.liveThreadsLayer), Layer.provide(layerUnavailable), + Layer.provide( + ServerConfig.layerTest(process.cwd(), { prefix: "t3-mcp-device-toolkit-test-" }), + ), Layer.provide(NodeServices.layer), ), ), diff --git a/apps/server/src/mcp/McpHttpServer.test.ts b/apps/server/src/mcp/McpHttpServer.test.ts index fb9ca151d360..330fc1840de6 100644 --- a/apps/server/src/mcp/McpHttpServer.test.ts +++ b/apps/server/src/mcp/McpHttpServer.test.ts @@ -17,8 +17,14 @@ import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; -import { McpProtocol, McpSchema, McpServer, Tool, Toolkit } from "effect/ai"; -import { HttpBody, HttpClient, HttpRouter, HttpServerResponse } from "effect/http"; +import { McpSchema, McpServer, Tool, Toolkit } from "effect/ai"; +import { + HttpBody, + HttpClient, + HttpClientRequest, + HttpRouter, + HttpServerResponse, +} from "effect/http"; import * as ProjectService from "../project/ProjectService.ts"; import * as ServerConfig from "../config.ts"; @@ -746,17 +752,38 @@ it.effect("sheds log entries before locators when every list is full", () => it.effect("terminates HTTP MCP sessions with DELETE", () => Effect.scoped( Effect.gen(function* () { - const layerServer = McpServer.layerHttp({ - name: "MCP termination test", - version: "1.0.0", - path: "/mcp", - protocols: [McpProtocol.v2025_06_18], - }); + const token = "providerTokenWithoutDots"; + const scope: McpInvocationContext.McpInvocationScope = { + environmentId, + requestNamespace: "provider-session", + thread: { + threadId: ThreadId.make("thread-provider"), + providerSessionId: "provider-session", + providerInstanceId: ProviderInstanceId.make("codex"), + }, + client: undefined, + capabilities: new Set(["orchestration"]), + issuedAt: 1, + }; + const layerServer = McpHttpServer.layerMcpTransport.pipe( + Layer.provide( + Layer.mock(McpSessionRegistry.McpSessionRegistry)({ + resolve: (presented) => + Effect.succeed( + presented === token + ? (scope as McpInvocationContext.McpThreadInvocationScope) + : undefined, + ), + }), + ), + ); yield* HttpRouter.serve(layerServer, { disableListenLog: true, disableLogger: true, }).pipe(Layer.build); - const httpClient = yield* HttpClient.HttpClient; + const httpClient = (yield* HttpClient.HttpClient).pipe( + HttpClient.mapRequest(HttpClientRequest.bearerToken(token)), + ); const initializeResponse = yield* httpClient.post("/mcp", { headers: { accept: "application/json, text/event-stream" }, diff --git a/apps/server/src/mcp/McpHttpServer.ts b/apps/server/src/mcp/McpHttpServer.ts index f4da6e051a7c..0e2b52696712 100644 --- a/apps/server/src/mcp/McpHttpServer.ts +++ b/apps/server/src/mcp/McpHttpServer.ts @@ -19,6 +19,7 @@ import packageJson from "../../package.json" with { type: "json" }; import * as ServerConfig from "../config.ts"; import * as DeviceService from "../device/DeviceService.ts"; import * as HtmlRender from "../htmlRender/HtmlRender.ts"; +import * as ThreadCommandExecutor from "../orchestration-v2/ThreadCommandExecutor.ts"; import * as ThreadManagementService from "../orchestration-v2/ThreadManagementService.ts"; import * as McpInvocationContext from "./McpInvocationContext.ts"; import * as McpToolAccess from "./McpToolAccess.ts"; @@ -762,11 +763,25 @@ const registerHtmlPreview = Effect.fn("McpHttpServer.registerHtmlPreview")(funct /** * `McpServer.toolkit` for handlers that declared their access (see * `McpToolAccess`). Every toolkit on `/mcp` registers through this. + * + * `McpServer.toolkit` asks for every service its tools declare when it + * registers them, but the auth middleware provides `McpInvocationContext` to + * each request instead. Registration must not get one: the services it + * captures would replace the request's. */ export const toolkitRegistration = , EX, RX>( toolkit: Toolkit.Toolkit, handlers: McpToolAccess.HandlersLayer, -) => McpServer.toolkit(toolkit).pipe(Layer.provide(McpToolAccess.HandlersLayer.layer(handlers))); +) => { + const registration = McpServer.toolkit(toolkit); + // @effect-diagnostics-next-line unsafeEffectTypeAssertion:off - the auth middleware provides it per request. + const registered = registration as Layer.Layer< + never, + never, + Exclude, McpInvocationContext.McpInvocationContext> + >; + return registered.pipe(Layer.provide(McpToolAccess.HandlersLayer.layer(handlers))); +}; /** A hand-registered tool, also only with handlers that declared their access. */ const imageToolRegistration = , A, E, R, EX, RX>( @@ -811,10 +826,10 @@ const layerPreviewControlsRegistration = toolkitRegistration( PreviewControlsHandlers.layer, ); -const layerEnvironmentRegistration = toolkitRegistration( +export const layerEnvironmentToolkit = toolkitRegistration( EnvironmentToolkit, EnvironmentHandlers.layer, -); +).pipe(Layer.provide(ThreadCommandExecutor.layer)); const layerProjectRegistration = toolkitRegistration(ProjectToolkit, ProjectHandlers.layer); @@ -848,6 +863,7 @@ export const layerMcpTransport = McpServer.layerHttp({ version: packageJson.version, path: "/mcp", protocols: [McpProtocol.v2025_06_18], + allowSessionTermination: true, }).pipe(Layer.provide(layerMcpAuthMiddleware)); export const layer = Layer.mergeAll( @@ -856,7 +872,7 @@ export const layer = Layer.mergeAll( layerThreadToolkit, layerAttachmentToolkit, layerProjectRegistration, - layerEnvironmentRegistration, + layerEnvironmentToolkit, layerPreviewControlsRegistration, layerWorktreeToolkitRegistration, layerPullRequestsToolkit, diff --git a/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts b/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts index d232386dec88..8f709ecf8625 100644 --- a/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts +++ b/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts @@ -57,6 +57,7 @@ import { threadShellFromProjection } from "../orchestration-v2/ProjectionStore.t import * as EventSink from "../orchestration-v2/EventSink.ts"; import * as Orchestrator from "../orchestration-v2/Orchestrator.ts"; import * as ThreadManagementService from "../orchestration-v2/ThreadManagementService.ts"; +import * as ThreadSearch from "../orchestration-v2/ThreadSearch.ts"; import * as ProviderAdapterRegistry from "../orchestration-v2/ProviderAdapterRegistry.ts"; import * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests"; import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; @@ -680,6 +681,7 @@ describe("orchestrator MCP toolkit", () => { Layer.provide(layerRegistry), Layer.provide(layerProviderRegistry), Layer.provide(layerScheduledTaskStub), + Layer.provide(Layer.mock(ThreadSearch.ThreadSearch)({})), Layer.provide( Layer.mock(ProjectService.ProjectService)({ getById: (id) => diff --git a/apps/server/src/mcp/toolkits/core.test.ts b/apps/server/src/mcp/toolkits/core.test.ts index 9380a8d7af90..a938ba7fd999 100644 --- a/apps/server/src/mcp/toolkits/core.test.ts +++ b/apps/server/src/mcp/toolkits/core.test.ts @@ -31,6 +31,7 @@ import * as ServerSecretStore from "../../auth/ServerSecretStore.ts"; import * as ServerConfig from "../../config.ts"; import * as ProviderAdapterRegistry from "../../orchestration-v2/ProviderAdapterRegistry.ts"; import * as ThreadManagement from "../../orchestration-v2/ThreadManagementService.ts"; +import * as ThreadSearch from "../../orchestration-v2/ThreadSearch.ts"; import * as PreviewBrowser from "../../preview/PreviewBrowser.ts"; import * as ProjectService from "../../project/ProjectService.ts"; import * as ProviderRegistry from "../../provider/ProviderRegistry.ts"; @@ -69,6 +70,12 @@ import { htmlRenderFromToolItem } from "@t3tools/shared/toolOutput"; const decodeMcpAttachmentInput = Schema.decodeUnknownEffect(McpAttachmentInput); +// Registration asks for every service the thread tools declare; these cases call none that use them. +const layerThreadToolkit = McpHttpServer.layerThreadToolkit.pipe( + Layer.provide(Layer.mock(ThreadSearch.ThreadSearch)({})), + Layer.provide(Layer.mock(ScheduledTaskService.ScheduledTaskService)({})), +); + it("publishes unique tool names with reference-free object-root inputs", () => { const names = new Set(); for (const toolkit of [ @@ -152,7 +159,7 @@ it.effect("checks capability through the production registration", () => expect(result.structuredContent).toBeUndefined(); }).pipe( Effect.provide( - McpHttpServer.layerThreadToolkit.pipe( + layerThreadToolkit.pipe( Layer.provideMerge(McpServer.McpServer.layer), Layer.provide(NodeCrypto.layer), Layer.provide(McpToolAccessTestkit.liveThreadsLayer), @@ -193,7 +200,7 @@ it.effect("returns a bounded public failure without serializing storage causes", expect(validate({ sequence: "invalid" }).valid).toBe(false); }).pipe( Effect.provide( - McpHttpServer.layerThreadToolkit.pipe( + layerThreadToolkit.pipe( Layer.provideMerge(McpServer.McpServer.layer), Layer.provide(NodeCrypto.layer), Layer.provide( @@ -308,7 +315,7 @@ it.effect("returns invalid parameter errors through the production registration" expect(error._tag).toBe("InvalidParams"); }).pipe( Effect.provide( - McpHttpServer.layerThreadToolkit.pipe( + layerThreadToolkit.pipe( Layer.provideMerge(McpServer.McpServer.layer), Layer.provide(NodeCrypto.layer), Layer.provide(Layer.mock(ThreadManagement.ThreadManagementService)({})), @@ -333,7 +340,7 @@ it.effect("keeps unexpected handler defects private through the production regis ]); }).pipe( Effect.provide( - McpHttpServer.layerThreadToolkit.pipe( + layerThreadToolkit.pipe( Layer.provideMerge(McpServer.McpServer.layer), Layer.provide(NodeCrypto.layer), Layer.provide( @@ -451,7 +458,7 @@ it.effect("a client caller targets any thread within its ceiling and cannot act expect(declaredFailure(forked)).toMatchObject({ code: "target_required" }); }).pipe( Effect.provide( - McpHttpServer.layerThreadToolkit.pipe( + layerThreadToolkit.pipe( Layer.provideMerge(McpServer.McpServer.layer), Layer.provide(NodeCrypto.layer), Layer.provide( @@ -511,7 +518,7 @@ it.effect("a read-only client reads threads and is refused every write before it expect(dispatched).toEqual([]); }).pipe( Effect.provide( - McpHttpServer.layerThreadToolkit.pipe( + layerThreadToolkit.pipe( Layer.provideMerge(McpServer.McpServer.layer), Layer.provide(NodeCrypto.layer), Layer.provide( diff --git a/apps/server/src/mcp/toolkits/environment/handlers.test.ts b/apps/server/src/mcp/toolkits/environment/handlers.test.ts index e4ae660cc004..bf32658886a0 100644 --- a/apps/server/src/mcp/toolkits/environment/handlers.test.ts +++ b/apps/server/src/mcp/toolkits/environment/handlers.test.ts @@ -12,11 +12,13 @@ import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as Ref from "effect/Ref"; import * as Stream from "effect/Stream"; +import { McpSchema, McpServer } from "effect/ai"; import * as Environment from "../../../environment/ServerEnvironment.ts"; import * as ThreadCommandExecutor from "../../../orchestration-v2/ThreadCommandExecutor.ts"; import * as ThreadManagement from "../../../orchestration-v2/ThreadManagementService.ts"; import * as Settings from "../../../serverSettings.ts"; +import * as McpHttpServer from "../../McpHttpServer.ts"; import * as McpInvocationContext from "../../McpInvocationContext.ts"; import * as McpToolAccess from "../../McpToolAccess.ts"; import { liveThreadShell } from "../../McpToolAccess.testkit.ts"; @@ -25,6 +27,71 @@ import { EnvironmentToolkit } from "./tools.ts"; const environmentId = EnvironmentId.make("environment:preferences"); const threadId = ThreadId.make("thread:preferences"); +const caller: McpInvocationContext.McpInvocationScope = { + environmentId, + requestNamespace: "provider:preferences", + thread: { + threadId, + providerSessionId: "provider:preferences", + providerInstanceId: ProviderInstanceId.make("codex"), + }, + client: undefined, + capabilities: new Set(["orchestration" as const]), + issuedAt: 0, +}; +const layerEnvironment = Layer.mock(Environment.ServerEnvironment)({ + getDescriptor: Effect.succeed({ + environmentId, + label: "Test", + platform: { os: "linux", arch: "x64" }, + serverVersion: "0.0.0", + capabilities: { repositoryIdentity: false }, + }), +}); + +it.effect("updates preferences for a thread caller through the /mcp registration", () => + Effect.gen(function* () { + const server = yield* McpServer.McpServer; + const result = yield* server + .callTool({ + name: "t3_environment_preferences_update", + arguments: { newWorktreesStartFromOrigin: true }, + }) + .pipe( + Effect.provideService(McpInvocationContext.McpInvocationContext, caller), + Effect.provideService( + McpSchema.McpServerClient, + McpSchema.McpServerClient.of({ + clientId: 1, + protocolVersion: "2025-06-18", + clientCapabilities: {}, + clientInfo: { name: "preferences", version: "1" }, + initializePayload: { + protocolVersion: "2025-06-18", + capabilities: {}, + clientInfo: { name: "preferences", version: "1" }, + }, + getClient: Effect.die("unused"), + }), + ), + ); + expect(result.isError).toBe(false); + expect(result.structuredContent).toMatchObject({ newWorktreesStartFromOrigin: true }); + }).pipe( + Effect.provide( + McpHttpServer.layerEnvironmentToolkit.pipe( + Layer.provideMerge(McpServer.McpServer.layer), + Layer.provide( + Layer.mock(ThreadManagement.ThreadManagementService)({ + getThreadShell: (id) => Effect.succeed(liveThreadShell(id)), + }), + ), + Layer.provide(layerEnvironment), + Layer.provide(Settings.layerTest()), + ), + ), + ), +); it.effect("refuses a preferences update when the caller's turn ends while it waits", () => Effect.gen(function* () { diff --git a/apps/server/src/mcp/toolkits/worktree/registration.test.ts b/apps/server/src/mcp/toolkits/worktree/registration.test.ts index 588d11d9c715..91257c2fdb80 100644 --- a/apps/server/src/mcp/toolkits/worktree/registration.test.ts +++ b/apps/server/src/mcp/toolkits/worktree/registration.test.ts @@ -22,6 +22,13 @@ import * as ScheduledTaskService from "../../../scheduledTasks/ScheduledTaskServ import * as SecretRequests from "../../../secrets/SecretRequests.ts"; import * as ServerSettings from "../../../serverSettings.ts"; import * as VcsStatusBroadcaster from "../../../vcs/VcsStatusBroadcaster.ts"; +import * as ServerSecretStore from "../../../auth/ServerSecretStore.ts"; +import * as ManagedProjectFolders from "../../../project/ManagedProjectFolders.ts"; +import * as ThreadLaunchService from "../../../orchestration-v2/ThreadLaunchService.ts"; +import * as ThreadSearch from "../../../orchestration-v2/ThreadSearch.ts"; +import * as PreviewManager from "../../../preview/Manager.ts"; +import * as SourceControlRepositoryService from "../../../sourceControl/SourceControlRepositoryService.ts"; +import * as GitVcsDriver from "../../../vcs/GitVcsDriver.ts"; import * as McpHttpServer from "../../McpHttpServer.ts"; import * as McpSessionRegistry from "../../McpSessionRegistry.ts"; import * as PreviewAutomationBroker from "../../PreviewAutomationBroker.ts"; @@ -41,6 +48,13 @@ const layerStubServices = Layer.mergeAll( Layer.mock(GitWorkflowService.GitWorkflowService)({}), Layer.mock(ProjectSetupScriptRunner.ProjectSetupScriptRunner)({}), Layer.mock(VcsStatusBroadcaster.VcsStatusBroadcaster)({}), + Layer.mock(GitVcsDriver.GitVcsDriver)({}), + Layer.mock(ManagedProjectFolders.ManagedProjectFolders)({ namedProjectsRoot: "/unused" }), + Layer.mock(PreviewManager.PreviewManager)({}), + Layer.mock(ServerSecretStore.ServerSecretStore)({}), + Layer.mock(SourceControlRepositoryService.SourceControlRepositoryService)({}), + Layer.mock(ThreadLaunchService.ThreadLaunchService)({}), + Layer.mock(ThreadSearch.ThreadSearch)({}), ); const ToolsListPayload = Schema.fromJsonString( diff --git a/apps/server/src/observability/RpcInstrumentation.test.ts b/apps/server/src/observability/RpcInstrumentation.test.ts index 4ac5c52ee031..75dc6138bbdf 100644 --- a/apps/server/src/observability/RpcInstrumentation.test.ts +++ b/apps/server/src/observability/RpcInstrumentation.test.ts @@ -72,7 +72,7 @@ const rpcSpans = (ended: ReadonlyArray) => // RpcTest keeps Effect's own RpcServer/RpcClient spans, which ws.ts turns off with // `disableTracing: true`. Everything else comes from the middleware or the handlers. const appSpans = (ended: ReadonlyArray) => - ended.filter((span) => !/^Rpc(Server|Client)\./.test(span.name)); + ended.filter((span) => span.attributes.get("rpc.system.name") !== "effect_rpc"); const exitTag = (span: Tracer.NativeSpan | undefined) => span?.status._tag === "Ended" ? span.status.exit._tag : undefined; diff --git a/apps/server/src/orchestration-v2/FoundationPersistence.test.ts b/apps/server/src/orchestration-v2/FoundationPersistence.test.ts index 9f4549370377..9a1e43a8b44b 100644 --- a/apps/server/src/orchestration-v2/FoundationPersistence.test.ts +++ b/apps/server/src/orchestration-v2/FoundationPersistence.test.ts @@ -2354,14 +2354,15 @@ it.layer(layerTest)("orchestration V2 foundation persistence", (it) => { it.effect("does not emit a SQL span for an empty safety claim", () => Effect.gen(function* () { const outbox = yield* EffectOutbox.EffectOutboxV2; - const spans: Array = []; + const statements: Array = []; const tracer = Tracer.make({ span: (options) => { const span = new Tracer.NativeSpan(options); const end = span.end.bind(span); span.end = (endTime, exit) => { end(endTime, exit); - spans.push(span.name); + const query = span.attributes.get("db.query.text"); + if (query !== undefined) statements.push(query); }; return span; }, @@ -2372,7 +2373,7 @@ it.layer(layerTest)("orchestration V2 foundation persistence", (it) => { .pipe(Effect.withTracer(tracer)); assert.isTrue(Option.isNone(claim)); - assert.notInclude(spans, "sql.execute"); + assert.deepEqual(statements, []); }).pipe(Effect.provide(Layer.fresh(layerEffectOutboxProvided))), ); diff --git a/docs/operations/relay-observability.md b/docs/operations/relay-observability.md index dd8537abb132..848a676acc94 100644 --- a/docs/operations/relay-observability.md +++ b/docs/operations/relay-observability.md @@ -30,7 +30,7 @@ For example: ```apl ['t3-code-relay-traces-prod'] -| where name startswith 'http.server' +| where kind == 'server' | extend endpoint = column_ifexists('attributes.http.route', ''), customAttributes = column_ifexists('attributes.custom', dynamic({})) | project _time, name, trace_id, duration, diff --git a/infra/relay/src/hooks/HookForwarder.test.ts b/infra/relay/src/hooks/HookForwarder.test.ts index 132494d2e382..75c3f8f562d8 100644 --- a/infra/relay/src/hooks/HookForwarder.test.ts +++ b/infra/relay/src/hooks/HookForwarder.test.ts @@ -600,7 +600,7 @@ describe("HookForwarder", () => { expect(serialized).not.toContain("also-secret"); expect(serialized).not.toContain("header-secret"); const server = spans.find((span) => span.kind === "server"); - expect(server?.attributes.get("url.path")).toBe(`/v1/hooks/${endpointKey}/hook-1/`); + expect(server?.attributes.get("url.path")).toBe(`/v1/hooks/${endpointKey}/hook-1/redacted`); }), ); @@ -639,7 +639,7 @@ describe("HookForwarder", () => { const servers = spans.filter((span) => span.kind === "server"); expect(servers).toHaveLength(1); expect(servers[0]?.attributes.get("url.path")).toBe( - `/v1/hooks/${endpointKey}/hook-1/`, + `/v1/hooks/${endpointKey}/hook-1/redacted`, ); expect(spans.every((span) => span.traceId !== "11111111111111111111111111111111")).toBe(true); }), diff --git a/infra/relay/src/hooks/HookForwarder.ts b/infra/relay/src/hooks/HookForwarder.ts index a660f8993150..1114063a12aa 100644 --- a/infra/relay/src/hooks/HookForwarder.ts +++ b/infra/relay/src/hooks/HookForwarder.ts @@ -76,7 +76,8 @@ export const redactRelayHookUrl = (url: string): string => { const path = url.split("?", 1)[0] ?? url; const segments = path.split("/"); if (segments.length >= 6) { - segments[5] = ""; + // URL-safe, so span attributes built from the parsed URL keep it readable. + segments[5] = "redacted"; } return segments.join("/"); }; diff --git a/infra/relay/src/http/Api.test.ts b/infra/relay/src/http/Api.test.ts index 42f0239f5c87..48dda4ba9406 100644 --- a/infra/relay/src/http/Api.test.ts +++ b/infra/relay/src/http/Api.test.ts @@ -1186,12 +1186,13 @@ describe("relay request tracing", () => { Effect.provideService(HttpServerRequest.HttpServerRequest, request), ); - expect(spans.map((span) => span.name)).toEqual(["http.server POST", "relay.test.endpoint"]); + expect(spans.map((span) => span.name)).toEqual(["POST", "relay.test.endpoint"]); expect(spans[0]?.kind).toBe("server"); expect(spans[0]?.attributes.get("url.path")).toBe("/v1/mobile/devices"); expect(spans[0]?.attributes.get("http.response.status_code")).toBe(204); - expect(spans[0]?.attributes.get("http.request.header.authorization")).toBe(""); - expect(spans[0]?.attributes.get("http.request.header.dpop")).toBe(""); + // Headers are not captured at all, so credentials never reach the span. + expect(spans[0]?.attributes.get("http.request.header.authorization")).toBeUndefined(); + expect(spans[0]?.attributes.get("http.request.header.dpop")).toBeUndefined(); expect(Option.isNone(spans[0]!.parent)).toBe(true); expect(Option.getOrUndefined(spans[1]!.parent)?.spanId).toBe(spans[0]?.spanId); }), diff --git a/packages/client-runtime/src/connection/outdatedHostUpdate.ts b/packages/client-runtime/src/connection/outdatedHostUpdate.ts index ebb3749c7419..b406c6957453 100644 --- a/packages/client-runtime/src/connection/outdatedHostUpdate.ts +++ b/packages/client-runtime/src/connection/outdatedHostUpdate.ts @@ -21,7 +21,7 @@ import * as RpcSerialization from "effect/rpc/RpcSerialization"; import * as Socket from "effect/socket/Socket"; import { fetchRemoteEnvironmentDescriptor } from "../environment/descriptor.ts"; -import { makeWsRpcProtocolClient } from "../rpc/protocol.ts"; +import { makeWsRpcProtocolClient, PING_TIMEOUT } from "../rpc/protocol.ts"; import { isLegacyUpdateHandoffLoss, resolveServerUpdateProgressResult } from "../state/server.ts"; import * as RelayEnvironmentDiscovery from "../relay/discovery.ts"; import * as ConnectionResolver from "./resolver.ts"; @@ -87,6 +87,7 @@ export const updateOutdatedHost = Effect.fn("clientRuntime.connection.updateOutd Layer.effect( RpcClient.Protocol, RpcClient.makeProtocolSocket({ + pingTimeout: PING_TIMEOUT, retryTransientErrors: false, retryPolicy: Schedule.recurs(0), }), diff --git a/packages/client-runtime/src/rpc/protocol.ts b/packages/client-runtime/src/rpc/protocol.ts index bc4666808ef9..e8aea66d3d89 100644 --- a/packages/client-runtime/src/rpc/protocol.ts +++ b/packages/client-runtime/src/rpc/protocol.ts @@ -6,3 +6,7 @@ export const makeWsRpcProtocolClient = RpcClient.make(WsRpcGroup); type RpcClientFactory = typeof makeWsRpcProtocolClient; export type WsRpcProtocolClient = RpcClientFactory extends Effect.Effect ? Client : never; + +// Pings go out every 5 seconds, and a server busy for a few seconds stops +// answering them. Allow 15 seconds without any frame before dropping the socket. +export const PING_TIMEOUT = "15 seconds"; diff --git a/packages/client-runtime/src/rpc/session.test.ts b/packages/client-runtime/src/rpc/session.test.ts index 4fafaea9306d..38e1b7f89d31 100644 --- a/packages/client-runtime/src/rpc/session.test.ts +++ b/packages/client-runtime/src/rpc/session.test.ts @@ -1085,7 +1085,7 @@ describe("RpcSessionFactory", () => { }), ); - it.effect("tolerates two missed pong windows before closing the session", () => + it.effect("closes the session after 15 seconds without a frame from the server", () => Effect.gen(function* () { const { factory, sockets } = yield* makeFactory(); const session = yield* factory.connect(PREPARED); diff --git a/packages/client-runtime/src/rpc/session.ts b/packages/client-runtime/src/rpc/session.ts index dbde036ccb28..9f83e7c6019d 100644 --- a/packages/client-runtime/src/rpc/session.ts +++ b/packages/client-runtime/src/rpc/session.ts @@ -24,7 +24,7 @@ import * as RpcClientError from "effect/rpc/RpcClientError"; import * as RpcSerialization from "effect/rpc/RpcSerialization"; import * as Socket from "effect/socket/Socket"; -import { makeWsRpcProtocolClient, type WsRpcProtocolClient } from "./protocol.ts"; +import { makeWsRpcProtocolClient, PING_TIMEOUT, type WsRpcProtocolClient } from "./protocol.ts"; import { NETWORK_BLOCKING_HINT } from "../errors/network.ts"; import type { ConnectionAttemptError, @@ -170,7 +170,7 @@ export const make = Effect.fn("RpcSessionFactory.make")(function* ( const connected = yield* Deferred.make(); const disconnected = yield* Deferred.make(); - // Set when the socket closes because pongs stopped, so the failure says so + // Set when the socket closes because the server stopped answering, so the failure says so // instead of looking like the server closed the connection. const pingTimedOut = yield* Ref.make(false); const hooks = RpcClient.ConnectionHooks.of({ @@ -201,6 +201,7 @@ export const make = Effect.fn("RpcSessionFactory.make")(function* ( const layerProtocol = Layer.effect( RpcClient.Protocol, RpcClient.makeProtocolSocket({ + pingTimeout: PING_TIMEOUT, retryTransientErrors: false, retryPolicy: Schedule.recurs(0), }), diff --git a/packages/shared/src/otelEnvironment.test.ts b/packages/shared/src/otelEnvironment.test.ts index 47f05c786015..7ad11ba20b4d 100644 --- a/packages/shared/src/otelEnvironment.test.ts +++ b/packages/shared/src/otelEnvironment.test.ts @@ -501,7 +501,10 @@ describe("OtelEnvironment", () => { ), ); assert.deepStrictEqual( - resource.attributes.map((attribute) => attribute.key), + resource.attributes + .map((attribute) => attribute.key) + // Effect adds these to every resource. + .filter((key) => !key.startsWith("telemetry.sdk.")), [...attributes, "service.name"], ); }), diff --git a/patches/@effect__vitest@4.0.1.patch b/patches/@effect__vitest@4.0.2.patch similarity index 84% rename from patches/@effect__vitest@4.0.1.patch rename to patches/@effect__vitest@4.0.2.patch index f11210fb9794..4057b3ffb89b 100644 --- a/patches/@effect__vitest@4.0.1.patch +++ b/patches/@effect__vitest@4.0.2.patch @@ -1,8 +1,8 @@ diff --git a/dist/index.d.ts b/dist/index.d.ts -index 530795c..237d090 100644 +index 845cb4c..24ede42 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts -@@ -7,14 +7,14 @@ import type * as Effect from "effect/Effect"; +@@ -10,14 +10,14 @@ import type * as Effect from "effect/Effect"; import type * as Layer from "effect/Layer"; import type * as Schema from "effect/Schema"; import type * as Scope from "effect/Scope"; @@ -17,10 +17,10 @@ index 530795c..237d090 100644 -export * from "vitest"; +export * from "vite-plus/test"; /** - * @stability unstable - * @since 4.0.0 + * Vitest test API without additional fixtures. + * diff --git a/dist/index.js b/dist/index.js -index 740429f..b5c69b8 100644 +index 178cc74..9289296 100644 --- a/dist/index.js +++ b/dist/index.js @@ -5,7 +5,7 @@ import * as internal from "./internal/internal.js"; @@ -30,8 +30,8 @@ index 740429f..b5c69b8 100644 -export * from "vitest"; +export * from "vite-plus/test"; /** - * @since 4.0.0 - */ + * Invokes the Effect Vitest equality-tester registration hook. + * diff --git a/dist/internal/internal.js b/dist/internal/internal.js index 7138319..4092c67 100644 --- a/dist/internal/internal.js @@ -46,7 +46,7 @@ index 7138319..4092c67 100644 const runPromise = /*#__PURE__*/Effect.fnUntraced(function* (effect, _ctx) { const exit = yield* Effect.exit(effect); diff --git a/dist/utils.js b/dist/utils.js -index 75a243d..08a4c89 100644 +index b97edbe..7dfd64a 100644 --- a/dist/utils.js +++ b/dist/utils.js @@ -4,7 +4,7 @@ import * as Option from "effect/Option"; @@ -59,7 +59,7 @@ index 75a243d..08a4c89 100644 // Primitives // ---------------------------- diff --git a/package.json b/package.json -index d64c43d..939548e 100644 +index 7dbb505..38d13cb 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ @@ -67,14 +67,14 @@ index d64c43d..939548e 100644 }, "peerDependencies": { - "vitest": ">=5.0.0 <6.0.0", - "effect": "^4.0.1" + "effect": "^4.0.2" }, "devDependencies": { diff --git a/src/index.ts b/src/index.ts -index 20c0497..2b16d32 100644 +index 9d65132..4aadda8 100644 --- a/src/index.ts +++ b/src/index.ts -@@ -7,7 +7,7 @@ import type * as Effect from "effect/Effect" +@@ -10,7 +10,7 @@ import type * as Effect from "effect/Effect" import type * as Layer from "effect/Layer" import type * as Schema from "effect/Schema" import type * as Scope from "effect/Scope" @@ -83,7 +83,7 @@ index 20c0497..2b16d32 100644 import * as internal from "./internal/internal.ts" /** -@@ -16,7 +16,7 @@ import * as internal from "./internal/internal.ts" +@@ -19,7 +19,7 @@ import * as internal from "./internal/internal.ts" * @stability unstable * @since 4.0.0 */ @@ -91,7 +91,7 @@ index 20c0497..2b16d32 100644 +export * from "vite-plus/test" /** - * @stability unstable + * Vitest test API without additional fixtures. diff --git a/src/internal/internal.ts b/src/internal/internal.ts index b43385b..4da79fd 100644 --- a/src/internal/internal.ts @@ -106,10 +106,10 @@ index b43385b..4da79fd 100644 const getCurrentSuite = V.TestRunner.getCurrentSuite diff --git a/src/utils.ts b/src/utils.ts -index 8ce3409..259be29 100644 +index 4473632..2d04404 100644 --- a/src/utils.ts +++ b/src/utils.ts -@@ -16,7 +16,7 @@ import * as Option from "effect/Option" +@@ -17,7 +17,7 @@ import * as Option from "effect/Option" import * as Predicate from "effect/Predicate" import * as Result from "effect/Result" import * as assert from "node:assert" diff --git a/patches/effect@4.0.1.patch b/patches/effect@4.0.1.patch deleted file mode 100644 index f0fb4d2bbceb..000000000000 --- a/patches/effect@4.0.1.patch +++ /dev/null @@ -1,139 +0,0 @@ -diff --git a/dist/ai/McpServer.js b/dist/ai/McpServer.js -index 2da594107273980144e84ab9b53636ef34043f6b..9fada51944f58fd0d988ae8f6b18f83e79583a25 100644 ---- a/dist/ai/McpServer.js -+++ b/dist/ai/McpServer.js -@@ -1093,7 +1093,7 @@ export const layerHttp = options => { - const methodNotAllowed = request => isAllowedMcpOrigin(request, options.allowedOrigins) ? Effect.succeed(methodNotAllowedResponse) : Effect.succeed(HttpServerResponse.empty({ - status: 403 - })); -- const routes = Layer.mergeAll(HttpRouter.add("GET", options.path, methodNotAllowed), HttpRouter.add("PUT", options.path, methodNotAllowed), HttpRouter.add("PATCH", options.path, methodNotAllowed), HttpRouter.add("DELETE", options.path, methodNotAllowed), HttpRouter.add("OPTIONS", options.path, methodNotAllowed)); -+ const routes = Layer.mergeAll(HttpRouter.add("GET", options.path, methodNotAllowed), HttpRouter.add("PUT", options.path, methodNotAllowed), HttpRouter.add("PATCH", options.path, methodNotAllowed), HttpRouter.add("OPTIONS", options.path, methodNotAllowed)); - return Layer.merge(layerWithRuntime(options, "http"), routes).pipe(Layer.provide(layerMcpProtocolHttp(options)), Layer.provide(runtime), Layer.provide(RpcSerialization.layerJsonRpc())); - }; - const layerMcpProtocolHttp = options => Layer.effect(RpcServer.Protocol)(Effect.gen(function* () { -@@ -1136,6 +1136,17 @@ const layerMcpProtocolHttp = options => Layer.effect(RpcServer.Protocol)(Effect. - }))) : response; - }); - }); -+ yield* router.add("DELETE", options.path, request => { -+ if (!isAllowedMcpOrigin(request, options.allowedOrigins)) { -+ return Effect.succeed(HttpServerResponse.empty({ -+ status: 403 -+ })); -+ } -+ const sessionId = request.headers[MCP_SESSION_ID_HEADER]; -+ return Effect.succeed(HttpServerResponse.empty({ -+ status: sessionId === undefined ? 400 : runtime.terminateSession(sessionId) ? 204 : 404 -+ })); -+ }); - return protocol; - })); - const mcpHttpSerialization = /*#__PURE__*/(() => { -diff --git a/dist/ai/internal/mcpRuntime.js b/dist/ai/internal/mcpRuntime.js -index 06370fd1341ae7a6b681c513a3a3c2866a414b7b..baa945b8b4763b5472c4a900b90b817209caa4bf 100644 ---- a/dist/ai/internal/mcpRuntime.js -+++ b/dist/ai/internal/mcpRuntime.js -@@ -336,6 +336,7 @@ export const make = /*#__PURE__*/Effect.fnUntraced(function* (protocols) { - }, - effectLogLevel: (clientId, headers, fallback) => stateful?.effectLogLevel(clientId, headers, fallback) ?? fallback, - disconnect: clientId => stateful?.disconnect(clientId), -+ terminateSession: sessionId => stateful?.terminateSession(sessionId) ?? false, - deliveryClientIds: () => stateful?.initializedClientIds() ?? [], - canDeliver: (clientId, headers, notification, fallback) => stateful?.canDeliver(clientId, headers, notification, fallback) ?? true, - installHandlers: Effect.fnUntraced(function* (options) { -diff --git a/dist/ai/internal/mcpStatefulRuntime.js b/dist/ai/internal/mcpStatefulRuntime.js -index 9471bdfbcff0643b44df3a08c3701f743a299638..455860316ddfccaf98ff95f9f05da500c331869c 100644 ---- a/dist/ai/internal/mcpStatefulRuntime.js -+++ b/dist/ai/internal/mcpStatefulRuntime.js -@@ -46,6 +46,7 @@ export const make = () => { - }, - resolve: resolveSession, - resolveSessionId: sessionId => bySessionId.get(sessionId), -+ terminateSession: sessionId => bySessionId.delete(sessionId), - setLogLevel: (level, clientId, headers) => Effect.sync(() => { - const session = resolveSession(clientId, headers); - if (session !== undefined) { -diff --git a/dist/http/HttpClientResponse.js b/dist/http/HttpClientResponse.js -index 42fba673de3b6bdc089c305f2e87ad56c3bfc374..2b4981b8a88606fb93d62a29f21f9556803307c4 100644 ---- a/dist/http/HttpClientResponse.js -+++ b/dist/http/HttpClientResponse.js -@@ -195,7 +195,7 @@ class WebHttpClientResponse extends Inspectable.Class { - if (this.cachedCookies) { - return this.cachedCookies; - } -- return this.cachedCookies = Cookies.fromSetCookie(this.source.headers.getSetCookie()); -+ return this.cachedCookies = Cookies.fromSetCookie(this.source.headers.getSetCookie?.() ?? []); - } - get remoteAddress() { - return Option.none(); -diff --git a/dist/rpc/RpcClient.d.ts b/dist/rpc/RpcClient.d.ts -index c498ff74510b784dcba791dbf6b8f23dac40a71d..8be9c910a8112a5289159251846128cd27cf32aa 100644 ---- a/dist/rpc/RpcClient.d.ts -+++ b/dist/rpc/RpcClient.d.ts -@@ -302,6 +302,8 @@ export declare const layerProtocolWorker: (options: { - declare const ConnectionHooks_base: Context.ServiceClass; - readonly onDisconnect: Effect.Effect; -+ /** Runs when the socket is dropped because pongs stopped, before `onDisconnect`. */ -+ readonly onPingTimeout?: Effect.Effect | undefined; - }>; - /** - * Represents optional client protocol hooks that run when a transport connects -diff --git a/dist/rpc/RpcClient.js b/dist/rpc/RpcClient.js -index 8af2e2efa9311d582add61fef131fd39fd459ab4..723649f05baa61ac33c9bc3634d106caa9641164 100644 ---- a/dist/rpc/RpcClient.js -+++ b/dist/rpc/RpcClient.js -@@ -679,11 +679,11 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun - yield* processData(frames[i]); - } - } -- }).pipe(Effect.scoped, Effect.raceFirst(Effect.flatMap(pinger.timeout, () => Effect.fail(new Socket.SocketError({ -+ }).pipe(Effect.scoped, Effect.raceFirst(Effect.flatMap(pinger.timeout, () => (Option.isSome(hooks) && hooks.value.onPingTimeout ? hooks.value.onPingTimeout : Effect.void).pipe(Effect.andThen(Effect.fail(new Socket.SocketError({ - reason: new Socket.SocketReadError({ - cause: new Error("ping timeout") - }) -- }))))); -+ }))))))); - }).pipe(Option.isSome(hooks) ? Effect.ensuring(hooks.value.onDisconnect) : identity, Effect.tapCause(cause => { - const error = Cause.findError(cause); - const hasError = Result.isSuccess(error); -@@ -726,17 +726,25 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun - const defaultRetryPolicy = /*#__PURE__*/Schedule.min([/*#__PURE__*/Schedule.exponential(500, 1.5), /*#__PURE__*/Schedule.spaced(5000)]); - const makePinger = /*#__PURE__*/Effect.fnUntraced(function* (writePing) { - let recievedPong = true; -+ let missedPongs = 0; - const latch = Latch.makeUnsafe(); - const reset = () => { - recievedPong = true; -+ missedPongs = 0; - latch.closeUnsafe(); - }; - const onPong = () => { - recievedPong = true; -+ missedPongs = 0; - }; - yield* Effect.suspend(() => { -- if (!recievedPong) return latch.open; -+ if (!recievedPong) { -+ missedPongs += 1; -+ if (missedPongs >= 3) return latch.open; -+ return writePing; -+ } - recievedPong = false; -+ missedPongs = 0; - return writePing; - }).pipe(Effect.delay("5 seconds"), Effect.ignore, Effect.forever, Effect.interruptible, Effect.forkScoped); - return { -diff --git a/src/http/HttpClientResponse.ts b/src/http/HttpClientResponse.ts -index c037015185da1e0c664653b5a496832a9449f3c1..a8b0d8be28bbbd3a0d0d3f1a222c2f074d2d3ad5 100644 ---- a/src/http/HttpClientResponse.ts -+++ b/src/http/HttpClientResponse.ts -@@ -341,7 +341,7 @@ class WebHttpClientResponse extends Inspectable.Class implements HttpClientRespo - if (this.cachedCookies) { - return this.cachedCookies - } -- return this.cachedCookies = Cookies.fromSetCookie(this.source.headers.getSetCookie()) -+ return this.cachedCookies = Cookies.fromSetCookie(this.source.headers.getSetCookie?.() ?? []) - } - - get remoteAddress(): Option.Option { diff --git a/patches/effect@4.0.2.patch b/patches/effect@4.0.2.patch new file mode 100644 index 000000000000..34bc39e02d46 --- /dev/null +++ b/patches/effect@4.0.2.patch @@ -0,0 +1,31 @@ +diff --git a/dist/rpc/RpcClient.d.ts b/dist/rpc/RpcClient.d.ts +index ebfe86f..3ff235d 100644 +--- a/dist/rpc/RpcClient.d.ts ++++ b/dist/rpc/RpcClient.d.ts +@@ -320,6 +320,8 @@ export declare const layerProtocolWorker: (options: { + declare const ConnectionHooks_base: Context.ServiceClass; + readonly onDisconnect: Effect.Effect; ++ /** Runs when the socket is dropped because the server stopped answering, before `onDisconnect`. */ ++ readonly onPingTimeout?: Effect.Effect | undefined; + }>; + /** + * Represents optional client protocol hooks that run when a transport connects +diff --git a/dist/rpc/RpcClient.js b/dist/rpc/RpcClient.js +index f5b14ef..cccf0a4 100644 +--- a/dist/rpc/RpcClient.js ++++ b/dist/rpc/RpcClient.js +@@ -688,11 +688,11 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun + yield* processData(frames[i]); + } + } +- }).pipe(Effect.scoped, Effect.raceFirst(Effect.flatMap(pinger.timeout, () => Effect.fail(new Socket.SocketError({ ++ }).pipe(Effect.scoped, Effect.raceFirst(Effect.flatMap(pinger.timeout, () => (Option.isSome(hooks) && hooks.value.onPingTimeout ? hooks.value.onPingTimeout : Effect.void).pipe(Effect.andThen(Effect.fail(new Socket.SocketError({ + reason: new Socket.SocketReadError({ + cause: new Error("ping timeout") + }) +- }))))); ++ }))))))); + }).pipe(Option.isSome(hooks) ? Effect.ensuring(hooks.value.onDisconnect) : identity, Effect.tapCause(cause => { + const error = Cause.findError(cause); + const hasError = Result.isSuccess(error); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 711ef69a8bbd..a74905c8cd80 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,11 +7,11 @@ settings: catalogs: default: '@effect/openapi-generator': - specifier: 4.0.1 - version: 4.0.1 + specifier: 4.0.2 + version: 4.0.2 '@effect/sql-sqlite-do': - specifier: 4.0.1 - version: 4.0.1 + specifier: 4.0.2 + version: 4.0.2 '@effect/tsgo': specifier: 0.46.1 version: 0.46.1 @@ -96,11 +96,11 @@ overrides: '@clerk/expo': 4.6.8 '@clerk/react': 6.16.1 '@clerk/shared': 4.33.0 - '@effect/atom-react': 4.0.1 - '@effect/platform-node': 4.0.1 - '@effect/platform-node-shared': 4.0.1 - '@effect/sql-pg': 4.0.1 - '@effect/vitest': 4.0.1 + '@effect/atom-react': 4.0.2 + '@effect/platform-node': 4.0.2 + '@effect/platform-node-shared': 4.0.2 + '@effect/sql-pg': 4.0.2 + '@effect/vitest': 4.0.2 '@effect/vitest>vitest': '-' '@expo/dom-webview': 58.0.2 '@expo/metro-config': 58.0.6 @@ -113,12 +113,13 @@ overrides: '@tailwindcss/oxide': 4.3.3 '@tailwindcss/vite': 4.3.3 '@types/node': 24.12.4 - effect: 4.0.1 - '@opencode/protocol>effect': 4.0.1 - '@opencode/schema>effect': 4.0.1 + effect: 4.0.2 + '@opencode/protocol>effect': 4.0.2 + '@opencode/schema>effect': 4.0.2 node-abi: 4.33.0 lightningcss: 1.33.0 tailwindcss: 4.3.3 + '@types/ws@^8': 8.18.2 undici@^8: 8.11.2 ws@^8: 8.22.0 vite: npm:@voidzero-dev/vite-plus-core@1.0.0 @@ -129,7 +130,7 @@ packageExtensionsChecksum: sha256-FYDaKefjFcda1+vgiq8yMpZC4ehE5CyLi/bsCGLgTtY= patchedDependencies: '@clerk/expo@4.6.8': b8b22ab733de5bea846ad9e88046c10582352fe9ad86f314f368b2a823f0f281 - '@effect/vitest@4.0.1': 359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2 + '@effect/vitest@4.0.2': 595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9 '@expo/metro-config@58.0.6': 90fa980e8b6480631fb2fbb04559834e3c612db40c550cd828f11be297d8d760 '@ff-labs/fff-node@0.9.4': c4e3cc2420ceb9dc650f9d189e9c24baf7e83f342998bacda5f459a4ce7927a8 '@legendapp/list@3.3.5': 680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806 @@ -142,7 +143,7 @@ patchedDependencies: '@react-navigation/native-stack@7.17.6': 3a53ec9b92f5db5f083f3ada585da75d62a13c82250c6ae47d92be91a828ad87 alchemy@2.0.0-beta.80: bea1b6c0c0b23157fd5f439a1d7feff430f28ac1621ba135d8a13eeec60ccacd dbus-next@0.10.2: cfff57561b0ee59b5addb3b2e6c6f20906e967507a530ab67e8db8108e520ba4 - effect@4.0.1: a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f + effect@4.0.2: ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688 expo-audio@58.0.4: c20cb927d6848c53e7bf6ec14fe12f19daae9210165d2dbaf4e0f97ce31e8742 expo-blur@58.0.3: d0311b0c0b05bd94393bd9a84653cb42503d5f3c8a19eb2627c34ace08bedf18 expo-glass-effect@58.0.3: f750041c45ab838a3530451dc7b50d6640a79141037a1b0499c5e3412a860c57 @@ -193,8 +194,8 @@ importers: specifier: 0.13.0 version: 0.13.0 '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@napi-rs/keyring': specifier: ^1.3.0 version: 1.3.0 @@ -217,8 +218,8 @@ importers: specifier: 0.10.2 version: 0.10.2(patch_hash=cfff57561b0ee59b5addb3b2e6c6f20906e967507a530ab67e8db8108e520ba4) effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) electron: specifier: 44.4.5 version: 44.4.5 @@ -239,8 +240,8 @@ importers: version: 0.1.44(react@19.2.6) devDependencies: '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 @@ -288,8 +289,8 @@ importers: specifier: 4.6.8 version: 4.6.8(patch_hash=b8b22ab733de5bea846ad9e88046c10582352fe9ad86f314f368b2a823f0f281)(9fda653cb5598772915b4a39a7aee91d) '@effect/atom-react': - specifier: 4.0.1 - version: 4.0.1(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(react@19.3.0)(scheduler@0.28.0) + specifier: 4.0.2 + version: 4.0.2(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(react@19.3.0)(scheduler@0.28.0) '@expo-google-fonts/dm-sans': specifier: ^0.4.2 version: 0.4.2 @@ -387,8 +388,8 @@ importers: specifier: 8.0.3 version: 8.0.3 effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) expo: specifier: 58.0.2 version: 58.0.2(bd4444e61fed72e958d45f73733b7f54) @@ -553,8 +554,8 @@ importers: version: 1.12.1(@expo/metro-config@58.0.6(patch_hash=90fa980e8b6480631fb2fbb04559834e3c612db40c550cd828f11be297d8d760)(bufferutil@4.1.0)(expo@58.0.2)(typescript@7.0.2)(utf-8-validate@6.0.6))(metro-cache@0.87.1)(metro-transform-worker@0.87.1(bufferutil@4.1.0)(utf-8-validate@6.0.6))(metro@0.87.1(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0)(tailwindcss@4.3.3) devDependencies: '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@pierre/trees': specifier: 1.0.0-beta.6 version: 1.0.0-beta.6(@pierre/theme@2.0.0)(@shikijs/themes@4.2.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(shiki@4.2.0) @@ -586,11 +587,11 @@ importers: specifier: 1.0.35 version: 1.0.35 '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/platform-node-shared': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(utf-8-validate@6.0.6) '@ff-labs/fff-node': specifier: 0.9.4 version: 0.9.4(patch_hash=c4e3cc2420ceb9dc650f9d189e9c24baf7e83f342998bacda5f459a4ce7927a8) @@ -631,8 +632,8 @@ importers: specifier: 8.0.3 version: 8.0.3 effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) jose: specifier: 'catalog:' version: 6.2.2 @@ -659,8 +660,8 @@ importers: version: 3.4.0 devDependencies: '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@modelcontextprotocol/sdk': specifier: 1.29.0 version: 1.29.0(zod@4.6.5) @@ -728,8 +729,8 @@ importers: specifier: ^3.2.2 version: 3.2.2(react@19.2.6) '@effect/atom-react': - specifier: 4.0.1 - version: 4.0.1(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(react@19.2.6)(scheduler@0.28.0) + specifier: 4.0.2 + version: 4.0.2(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(react@19.2.6)(scheduler@0.28.0) '@formkit/auto-animate': specifier: ^0.9.0 version: 0.9.0 @@ -827,8 +828,8 @@ importers: specifier: ^3.4.16 version: 3.4.16 effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) hast-util-to-html: specifier: ^9.0.5 version: 9.0.5 @@ -882,11 +883,11 @@ importers: version: 5.0.14(@types/react@19.3.0)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6)) devDependencies: '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@rolldown/plugin-babel': specifier: ^0.2.0 version: 0.2.3(@babel/core@7.29.7)(@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7))(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(rolldown@1.2.5) @@ -954,11 +955,11 @@ importers: specifier: 3.18.1 version: 3.18.1(react-dom@19.3.0(react@19.3.0))(react@19.2.6) '@effect/sql-pg': - specifier: 4.0.1 - version: 4.0.1(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) + specifier: 4.0.2 + version: 4.0.2(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) '@effect/sql-sqlite-do': specifier: 'catalog:' - version: 4.0.1(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) + version: 4.0.2(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) '@noble/curves': specifier: 'catalog:' version: 1.9.1 @@ -976,23 +977,23 @@ importers: version: link:../../packages/shared alchemy: specifier: 2.0.0-beta.80 - version: 2.0.0-beta.80(patch_hash=bea1b6c0c0b23157fd5f439a1d7feff430f28ac1621ba135d8a13eeec60ccacd)(d02b4e60a6a3c6b542dfa700209cf5ce) + version: 2.0.0-beta.80(patch_hash=bea1b6c0c0b23157fd5f439a1d7feff430f28ac1621ba135d8a13eeec60ccacd)(ba3b2ceb33761bc36b3beb11002eb316) drizzle-orm: specifier: 1.0.0-rc.5-ab785fc - version: 1.0.0-rc.5-ab785fc(74e00612579436038985093f7f46ef5c) + version: 1.0.0-rc.5-ab785fc(da2d75dec181fbafaeb6986c8741457b) effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) devDependencies: '@cloudflare/workers-types': specifier: ^4.20260601.1 version: 4.20260604.1 '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 @@ -1012,18 +1013,18 @@ importers: oxlint-plugin-t3code: dependencies: '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@oxlint/plugins': specifier: ^1.63.0 version: 1.68.0 effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) devDependencies: '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) vite-plus: specifier: 1.0.0 version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) @@ -1037,15 +1038,15 @@ importers: specifier: workspace:* version: link:../shared effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) three: specifier: 0.180.0 version: 0.180.0 devDependencies: '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/three': specifier: 0.180.0 version: 0.180.0 @@ -1056,12 +1057,12 @@ importers: packages/contracts: dependencies: effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) devDependencies: '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) vite-plus: specifier: 1.0.0 version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) @@ -1069,18 +1070,18 @@ importers: packages/effect-acp: dependencies: effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) devDependencies: '@effect/openapi-generator': specifier: 'catalog:' - version: 4.0.1(@effect/platform-node@4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6))(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(encoding@0.1.13) + version: 4.0.2(@effect/platform-node@4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6))(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(encoding@0.1.13) '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 @@ -1091,18 +1092,18 @@ importers: packages/effect-codex-app-server: dependencies: effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) devDependencies: '@effect/openapi-generator': specifier: 'catalog:' - version: 4.0.1(@effect/platform-node@4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6))(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(encoding@0.1.13) + version: 4.0.2(@effect/platform-node@4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6))(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(encoding@0.1.13) '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 @@ -1125,15 +1126,15 @@ importers: specifier: 8.0.3 version: 8.0.3 effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) devDependencies: '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 @@ -1162,15 +1163,15 @@ importers: specifier: workspace:* version: link:../shared effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) devDependencies: '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@t3tools/provider-testing': specifier: workspace:* version: link:../provider-testing @@ -1193,15 +1194,15 @@ importers: specifier: workspace:* version: link:../shared effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) devDependencies: '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 @@ -1227,18 +1228,18 @@ importers: specifier: workspace:* version: link:../shared effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) yaml: specifier: ^2.9.0 version: 2.9.0 devDependencies: '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@t3tools/provider-testing': specifier: workspace:* version: link:../provider-testing @@ -1264,15 +1265,15 @@ importers: specifier: workspace:* version: link:../shared effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) devDependencies: '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@t3tools/provider-testing': specifier: workspace:* version: link:../provider-testing @@ -1301,15 +1302,15 @@ importers: specifier: workspace:* version: link:../shared effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) devDependencies: '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@t3tools/provider-testing': specifier: workspace:* version: link:../provider-testing @@ -1327,7 +1328,7 @@ importers: version: 1.15.13 '@opencode/client': specifier: 2.0.23 - version: 2.0.23(patch_hash=f725d6e6d9af0d56dc6ed71b863518fbfc751af1773cff2311209d2c404cf4cd)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) + version: 2.0.23(patch_hash=f725d6e6d9af0d56dc6ed71b863518fbfc751af1773cff2311209d2c404cf4cd)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) '@opencode/protocol': specifier: 2.0.23 version: 2.0.23(patch_hash=17a82f6c2c98fd0371e786c211a4fbdaaba403577a25733e5e99a0c25a16a685) @@ -1344,15 +1345,15 @@ importers: specifier: workspace:* version: link:../shared effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) devDependencies: '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@t3tools/provider-testing': specifier: workspace:* version: link:../provider-testing @@ -1375,15 +1376,15 @@ importers: specifier: workspace:* version: link:../shared effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) devDependencies: '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@t3tools/provider-testing': specifier: workspace:* version: link:../provider-testing @@ -1397,8 +1398,8 @@ importers: packages/provider-testing: dependencies: '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@t3tools/contracts': specifier: workspace:* version: link:../contracts @@ -1409,12 +1410,12 @@ importers: specifier: workspace:* version: link:../shared effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) devDependencies: '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 @@ -1434,8 +1435,8 @@ importers: specifier: workspace:* version: link:../contracts effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) jose: specifier: 'catalog:' version: 6.2.2 @@ -1474,11 +1475,11 @@ importers: version: 2.9.0 devDependencies: '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 @@ -1498,15 +1499,15 @@ importers: specifier: workspace:* version: link:../shared effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) devDependencies: '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 @@ -1520,12 +1521,12 @@ importers: specifier: workspace:* version: link:../shared effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) devDependencies: '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 @@ -1536,8 +1537,8 @@ importers: scripts: dependencies: '@effect/platform-node': - specifier: 4.0.1 - version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) '@electron/asar': specifier: ^4.3.0 version: 4.3.0 @@ -1551,8 +1552,8 @@ importers: specifier: workspace:* version: link:../packages/tailscale effect: - specifier: 4.0.1 - version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) pngjs: specifier: 7.0.0 version: 7.0.0 @@ -1561,8 +1562,8 @@ importers: version: 0.35.4(@types/node@24.12.4) devDependencies: '@effect/vitest': - specifier: 4.0.1 - version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/pngjs': specifier: 6.0.5 version: 6.0.5 @@ -1606,8 +1607,8 @@ packages: peerDependencies: '@distilled.cloud/cloudflare': 1.0.0-rc.13 '@effect/platform-bun': ^4.0.0 - '@effect/platform-node': 4.0.1 - effect: 4.0.1 + '@effect/platform-node': 4.0.2 + effect: 4.0.2 rolldown: 1.2.5 vite: ^7.0.0 || ^8.0.0 peerDependenciesMeta: @@ -1623,7 +1624,7 @@ packages: '@alchemy.run/floci@2.0.0-beta.80': resolution: {integrity: sha512-gtUXFitVY9pSQQYCDtSVyjfd5YOKY3Wleg3WCG5yWno7/l7xBo4kfOMbM63tzWnmka9LewVSITbMfwnC4D/73A==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@alchemy.run/node-utils@2.0.0-beta.80': resolution: {integrity: sha512-uOhVDQZTjwh1AU4mrk0wB5rQaziTwSrVMfqwFP8wALfU//nveu4goOWZimHtqytXiybENh+iEbALH25EFAkc7A==} @@ -2661,82 +2662,82 @@ packages: '@distilled.cloud/acme@1.0.0-rc.13': resolution: {integrity: sha512-3rmzH125jQxk0zpCvXGNEybSaqpAolyCX+8LWDDrXf+wIKs67GflkJyQLjOOh0lKARe29hdUZGgqEbbvpp0LNA==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@distilled.cloud/aws@1.0.0-rc.13': resolution: {integrity: sha512-6ZGcp0LkniB0I2l8Gr7vKRQgTZTch0/07iuvuMZv9Nu+rWwNuZG9VHTUdlIoarWgCHFUNN8yvSYBGOjaeN99tQ==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@distilled.cloud/axiom@1.0.0-rc.13': resolution: {integrity: sha512-aRoJtvfXgWi5dtfjuJvDwnArRKvv2MPYHCxYeZcxZbqe62IaQA5yu/khft7TJvxeDyJoSDnVXZu3RyJjNmBA4g==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@distilled.cloud/cloudflare@1.0.0-rc.13': resolution: {integrity: sha512-Py5FoA6azyYExDb91VaeC+9x+ravOBSRhXeUmpcQjCtgqmOdiXgc2YZGMMPI68DF+kMuHRVIrR+2uRZNDOhYjw==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@distilled.cloud/core@1.0.0-rc.13': resolution: {integrity: sha512-RYyrxW+F5zrTuu1UWV/JoycholBCIkXIusYJdDniDwCdmbjdKGl07KlkkeeY774uBawNGf+pt1ESNGeLVnNHsg==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@distilled.cloud/doppler@1.0.0-rc.13': resolution: {integrity: sha512-GgOEUCc4Bx0C+lYNfZ4OqaA3ULvqDUQTJC8gobSh1L0jfrWclv/kRcSr43v4IXyStld+saUvDfDZgeGzlE74OA==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@distilled.cloud/fly-io@1.0.0-rc.13': resolution: {integrity: sha512-Ipp+9nvs+PzqwDPhZq2/BEHNUeWzXKhrvjfT/eBLFHo/QpH0HgdMH8MSQe6/LGszZoormwA6ZrTv2wxaFO7ZiQ==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@distilled.cloud/gcp@1.0.0-rc.13': resolution: {integrity: sha512-un8jqil9ejt96oN+OfEDUsbrEOpC6Ut4B/73DqKDttvt0Btal1OeBNTTQsftVCpeEzXAvkYKGRIT2AeEu1J5jA==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@distilled.cloud/hetzner@1.0.0-rc.13': resolution: {integrity: sha512-FvqofgMypNkeZbnjGjWMZqvqGd9GxcMzdbi/6JrbcPaHMJG+CCBGt/mAa7mLlEovzwVaGCsBnOXJ6rP5pZjB4Q==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@distilled.cloud/infisical@1.0.0-rc.13': resolution: {integrity: sha512-Zz8ksA/Y8UUUnWag2UBxB5WYA6wLqOS78ugWJ3cOr9Ot+4bC0NJYWanq35uFInozLXWhun84USU2wOI+mdwQUQ==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@distilled.cloud/neon@1.0.0-rc.13': resolution: {integrity: sha512-2JvbBFj1kBfkDVPnWcJZ7TJ1cJwWBtScAY7SNJ2qNBUuAK1Wf2kjJXU0/j16eCJ48BQigPArQFWL51wVu/EUQQ==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@distilled.cloud/planetscale@1.0.0-rc.13': resolution: {integrity: sha512-AM97YGRnDfZdqsxFgQMQ83p7ZlRqxS6/foTTJA5P590UMbbOyrQyFyd33BD0agul8a3ngZde3Oej6v7BN55aWw==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@distilled.cloud/prisma@1.0.0-rc.13': resolution: {integrity: sha512-+UmHSp9JV8XQl5t5tTdQBY+dWZ9abrEoc7leYEctA0Ar1TrKZEl2EaeQDoumrg4RyGaacRlirEbxaDFI4SOPtQ==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@distilled.cloud/railway@1.0.0-rc.13': resolution: {integrity: sha512-yXalRS3GFLo3hWM0HXptqudi23NVCM4FRD20db2/Ob9FFcNrMJik1fopv+74HOiV+BuxfkCP609KsEK6LVdB/g==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@distilled.cloud/stripe@1.0.0-rc.13': resolution: {integrity: sha512-jLTJ1mg4U39lHZb9EGYBJLR91yB3LSG6fVxix+r1BbqaTovWzE+NuJ7fu9fRSeax9jRAqsGLLxe9IxJbOtwQnQ==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@distilled.cloud/zerossl@1.0.0-rc.13': resolution: {integrity: sha512-dnxhg39lZjpTdZbHCVYPoUC9E2r8eSHbqiYDhSq2EurVPJcc9YRXlxZR1bP1nniSp078XV8GA+aUECt/Z56aQg==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@dnd-kit/accessibility@3.1.1': resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} @@ -2769,52 +2770,52 @@ packages: '@drizzle-team/brocli@0.12.0': resolution: {integrity: sha512-mlUE+rZ8CatQekLhnaiN91Iemdd+e2gFKooGlnRB3oPTL3VghLfX24dx7HrzMNeC1JrIB/0kpsfyty3f5HNfxQ==} - '@effect/atom-react@4.0.1': - resolution: {integrity: sha512-V/vcbFCVY2AV7PvjhmOP37Cr4NofrtxBBGW6M1ckBIBxmpi3sMwdPTxj2obPNE4Ho3gkeEQzRC9zEIQ4BDNdQg==} + '@effect/atom-react@4.0.2': + resolution: {integrity: sha512-rg9xnPHzjURhKhOm+bKnYitPPQC02m6R7ZmJLNdxtrNCgDQ9fSOPqZHN5W/GzUw+OsvplKSvaBe/JSQejeYKYA==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 react: '>=19.0.0 <20.0.0' scheduler: '>=0.25.0 <0.28.0' - '@effect/openapi-generator@4.0.1': - resolution: {integrity: sha512-UWlXX1cjP68FDQyc+mlwfh34OpCKlnAREFpWk9DG6fzmw/w/+FvIyadZvS1y7G2b8U2e/khO0f3p5ak0DTC9Ng==} + '@effect/openapi-generator@4.0.2': + resolution: {integrity: sha512-rSH4VFCPs8d0LnB4QMPaGpT3Z3KrF7mCV+hvpv8FDXerImpYMSEzaLN5WWB7LG+mOOcTWIa3KYWvBBf0kH/JrQ==} hasBin: true peerDependencies: - '@effect/platform-node': 4.0.1 - effect: 4.0.1 + '@effect/platform-node': 4.0.2 + effect: 4.0.2 - '@effect/platform-node-shared@4.0.1': - resolution: {integrity: sha512-VanaejudEhKJ4mpmqnEzdncVUhN6sGSO9ZDND54mRxAlZxj+4dYAWjDTYAy9o4tJo9aALeP2U8cVcOFzEdSeLg==} + '@effect/platform-node-shared@4.0.2': + resolution: {integrity: sha512-xn8aDw9p/QUPt1Yi0C3HUMcycT+MjHfAe19FDUDrUuzcwq0xxskkamVSqbdykF/Dsuus4aZTsCBZA/vkhHIfbQ==} engines: {node: '>=18.0.0'} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 - '@effect/platform-node@4.0.1': - resolution: {integrity: sha512-FT0Fu5LO0/XP9zBpj0HqrtJNLluK4StNKqf6TSulUsjp8FPBsDVXGJiDuyoN5J2yVMT7tgvhxSwaDcObWVRIaQ==} + '@effect/platform-node@4.0.2': + resolution: {integrity: sha512-RWesXBQStnFHdD465gR5thFKgXO7FSyCKRZ56nWXB7PedNVzr8vV0nb/3MULLdri493sMz/b/pmGBTb6K8cBjQ==} engines: {node: '>=18.0.0'} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 redis: '>=5.0.0 <7.0.0' '@effect/sql-d1@4.0.0': resolution: {integrity: sha512-pmkzncZCGD6CLyvc89keEBU85h+gHVCu/LKQnThd+ruYSUbG0R9jYA+lnk3R8Djp0wQW6SbsbmHQuhopIYyo3Q==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 - '@effect/sql-pg@4.0.1': - resolution: {integrity: sha512-X5aqOxHNWqVXL+vyTQZLYDip65lm5rrZHuNkWyyr672iup1tjYAs5Dn9tHR5h1t0blpJ57j8Ng5G5AUQ8teIqA==} + '@effect/sql-pg@4.0.2': + resolution: {integrity: sha512-i2N5F0L5s1S8Nx2I8V7k+awBinP26b5xiiHL1zn5b6RALNnloiU7FK1yS4dAkmqG9B0EcOVxvJrIpoMWwrB4hw==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@effect/sql-sqlite-bun@4.0.0-rc.112': resolution: {integrity: sha512-EqR8pWZo3VzedRvP6qo5HcfqNmFvSa+sQLWQl3oBMx2KY1FCc8NYjep79wYjNkW0qABnffrPUXCMTOncd1A+5g==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 - '@effect/sql-sqlite-do@4.0.1': - resolution: {integrity: sha512-tS+qQok3xKsycnnhvIDX+6nvlM4Y2ouvru3voOzxtDQqX96NSOMX2xReLf4Feb6GN3jnzgEEtVGDDWk3YLrpsg==} + '@effect/sql-sqlite-do@4.0.2': + resolution: {integrity: sha512-dbKYFZSfdaIbbnYgvu432f9ISi/nSaXCYRfoK0HWKB8NwqI7hspaFvc4cW6ETDJyaoCDJzKzBQxrvCmA/T0XHw==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@effect/tsgo-darwin-arm64@0.46.1': resolution: {integrity: sha512-ErehNqI9p8me4XKO8N4bzdDWgBbSNlJ3CIZGfXoAW6Fo4Oo7x4ZbZV7KNsMSn9aJSiUUOQUoXjGBdslU5OYMdw==} @@ -2855,10 +2856,10 @@ packages: resolution: {integrity: sha512-DMcBCg5nWLufejRlyqFEkXmbtaLUJ56NKny6L1LTtimvz0U3VNj9H6ZUXV4WeeWvJzVyL0y7G2wsCBAFUgy1jw==} hasBin: true - '@effect/vitest@4.0.1': - resolution: {integrity: sha512-DbY9kf/018zBxRIX7rJwBnPVPzSarnUpcshm0vfYPFiDkSa7AFrXTh2BlDNUbrdrmqcIOBhVZ7GkEtxf8WiQrg==} + '@effect/vitest@4.0.2': + resolution: {integrity: sha512-Zb72nu7iUy3rNdBp9J1Xu5oUQGwJ8cOYsUwmYgujj88aqyvASRDjOS01SjlcPViqf77csduetKdCJTVndMNfzw==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 '@electric-sql/pglite@0.3.15': resolution: {integrity: sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==} @@ -4200,7 +4201,7 @@ packages: '@opencode/client@2.0.23': resolution: {integrity: sha512-tHfYu7sKNxFu2dZ7Pu5vZG4T8GHqU71ZCsXHZ/RoI0LLdG7t2zwePHWR9aNbcSP+jDUGyaQlzr5utC9Uduczvg==} peerDependencies: - effect: 4.0.1 + effect: 4.0.2 solid-js: '>=1.9.0' peerDependenciesMeta: effect: @@ -6113,8 +6114,8 @@ packages: '@types/whatwg-url@11.0.5': resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} - '@types/ws@8.18.1': - resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@types/ws@8.18.2': + resolution: {integrity: sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==} '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -6766,15 +6767,15 @@ packages: '@alchemy.run/frontend-frameworks': 2.0.0-beta.80 '@aws/durable-execution-sdk-js': ^2.1.0 '@effect/platform-bun': ^4.0.0 - '@effect/platform-node': 4.0.1 + '@effect/platform-node': 4.0.2 '@effect/sql-mysql2': ^4.0.0 - '@effect/sql-pg': 4.0.1 - '@effect/vitest': 4.0.1 + '@effect/sql-pg': 4.0.2 + '@effect/vitest': 4.0.2 '@prisma/orm-postgres': 8.0.0-rc.11 '@vercel/nft': ^1.10.2 drizzle-kit: 1.0.0-rc.5-ab785fc drizzle-orm: 1.0.0-rc.5-ab785fc - effect: 4.0.1 + effect: 4.0.2 mongodb: ^6.10.0 mysql2: ^3.24.2 pg: ^8.22.0 @@ -7804,7 +7805,7 @@ packages: '@effect/sql-d1': '>=4.0.0-beta.105 || >=4.0.0' '@effect/sql-libsql': '>=4.0.0-beta.105 || >=4.0.0' '@effect/sql-mysql2': '>=4.0.0-beta.105 || >=4.0.0' - '@effect/sql-pg': 4.0.1 + '@effect/sql-pg': 4.0.2 '@effect/sql-pglite': '>=4.0.0-beta.105 || >=4.0.0' '@effect/sql-sqlite-bun': '>=4.0.0-beta.105 || >=4.0.0' '@effect/sql-sqlite-do': '>=4.0.0-beta.105 || >=4.0.0' @@ -7835,7 +7836,7 @@ packages: arktype: '>=2.0.0' better-sqlite3: '>=9.3.0' bun-types: '*' - effect: 4.0.1 + effect: 4.0.2 expo-sqlite: '>=14.0.0' minipg: '>=0.2.0' mssql: ^11.0.1 @@ -7962,8 +7963,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - effect@4.0.1: - resolution: {integrity: sha512-b1VlQG9g8fwxE5QnIZuPoOP/0MsqHJSRKxUijjoM80E5q95FXrX5yWtY5XI8G+GJsBQAVSDbLS458z9a++uVvw==} + effect@4.0.2: + resolution: {integrity: sha512-YeYnLqSFrL4+tCOXRrfNP03w8FILoF/wDl+ukraJAGzXrjcS383W7LAAvGBbLatEyktmLxRq+9xJinx7QzXW5A==} ejs@3.1.10: resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} @@ -12047,14 +12048,14 @@ snapshots: dependencies: json-schema: 0.4.0 - '@alchemy.run/cloudflare-runtime@2.0.0-beta.80(@distilled.cloud/cloudflare@1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)))(@effect/platform-node@4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6))(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(rolldown@1.2.5)(typescript@7.0.2)': + '@alchemy.run/cloudflare-runtime@2.0.0-beta.80(@distilled.cloud/cloudflare@1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)))(@effect/platform-node@4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6))(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(rolldown@1.2.5)(typescript@7.0.2)': dependencies: '@alchemy.run/node-utils': 2.0.0-beta.80 '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260918.1) - '@distilled.cloud/cloudflare': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) + '@distilled.cloud/cloudflare': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) '@puppeteer/browsers': 3.2.2(yauzl@3.4.0) capnp-es: 0.0.16(typescript@7.0.2) - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) magic-string: 0.30.21 mime: 4.1.0 sharp: 0.35.4(@types/node@24.12.4) @@ -12062,7 +12063,7 @@ snapshots: workerd: 1.20260918.1 yauzl: 3.4.0 optionalDependencies: - '@effect/platform-node': 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) + '@effect/platform-node': 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) rolldown: 1.2.5 vite: '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' transitivePeerDependencies: @@ -12070,9 +12071,9 @@ snapshots: - proxy-agent - typescript - '@alchemy.run/floci@2.0.0-beta.80(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@alchemy.run/floci@2.0.0-beta.80(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) '@alchemy.run/node-utils@2.0.0-beta.80': dependencies: @@ -13354,86 +13355,86 @@ snapshots: '@dimforge/rapier3d-compat@0.12.0': {} - '@distilled.cloud/acme@1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@distilled.cloud/acme@1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) - '@distilled.cloud/aws@1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@distilled.cloud/aws@1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) + '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) '@smithy/types': 4.18.0 - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) - '@distilled.cloud/axiom@1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@distilled.cloud/axiom@1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) - '@distilled.cloud/cloudflare@1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@distilled.cloud/cloudflare@1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) - '@distilled.cloud/core@1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@distilled.cloud/core@1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) graphql: 16.11.0 - '@distilled.cloud/doppler@1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@distilled.cloud/doppler@1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) - '@distilled.cloud/fly-io@1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@distilled.cloud/fly-io@1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) - '@distilled.cloud/gcp@1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@distilled.cloud/gcp@1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) - '@distilled.cloud/hetzner@1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@distilled.cloud/hetzner@1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) - '@distilled.cloud/infisical@1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@distilled.cloud/infisical@1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) - '@distilled.cloud/neon@1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@distilled.cloud/neon@1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) - '@distilled.cloud/planetscale@1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@distilled.cloud/planetscale@1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) - '@distilled.cloud/prisma@1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@distilled.cloud/prisma@1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) - '@distilled.cloud/railway@1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@distilled.cloud/railway@1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) - '@distilled.cloud/stripe@1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@distilled.cloud/stripe@1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) - '@distilled.cloud/zerossl@1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@distilled.cloud/zerossl@1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) '@dnd-kit/accessibility@3.1.1(react@19.2.6)': dependencies: @@ -13469,62 +13470,62 @@ snapshots: '@drizzle-team/brocli@0.12.0': {} - '@effect/atom-react@4.0.1(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(react@19.2.6)(scheduler@0.28.0)': + '@effect/atom-react@4.0.2(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(react@19.2.6)(scheduler@0.28.0)': dependencies: - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) react: 19.2.6 scheduler: 0.28.0 - '@effect/atom-react@4.0.1(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(react@19.3.0)(scheduler@0.28.0)': + '@effect/atom-react@4.0.2(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(react@19.3.0)(scheduler@0.28.0)': dependencies: - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) react: 19.3.0 scheduler: 0.28.0 - '@effect/openapi-generator@4.0.1(@effect/platform-node@4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6))(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(encoding@0.1.13)': + '@effect/openapi-generator@4.0.2(@effect/platform-node@4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6))(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(encoding@0.1.13)': dependencies: - '@effect/platform-node': 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + '@effect/platform-node': 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) swagger2openapi: 7.0.8(encoding@0.1.13) transitivePeerDependencies: - encoding - '@effect/platform-node-shared@4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(utf-8-validate@6.0.6)': + '@effect/platform-node-shared@4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(utf-8-validate@6.0.6)': dependencies: - '@types/ws': 8.18.1 - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + '@types/ws': 8.18.2 + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) ws: 8.22.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform-node@4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6)': + '@effect/platform-node@4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6)': dependencies: - '@effect/platform-node-shared': 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(utf-8-validate@6.0.6) - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + '@effect/platform-node-shared': 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(utf-8-validate@6.0.6) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) redis: 6.2.1 undici: 8.11.2 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/sql-d1@4.0.0(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@effect/sql-d1@4.0.0(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: '@cloudflare/workers-types': 5.20261004.1 - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) - '@effect/sql-pg@4.0.1(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@effect/sql-pg@4.0.2(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) - '@effect/sql-sqlite-bun@4.0.0-rc.112(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@effect/sql-sqlite-bun@4.0.0-rc.112(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) optional: true - '@effect/sql-sqlite-do@4.0.1(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@effect/sql-sqlite-do@4.0.2(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) '@effect/tsgo-darwin-arm64@0.46.1': optional: true @@ -13557,9 +13558,9 @@ snapshots: '@effect/tsgo-win32-arm64': 0.46.1 '@effect/tsgo-win32-x64': 0.46.1 - '@effect/vitest@4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)': + '@effect/vitest@4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)': dependencies: - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) vite-plus: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) transitivePeerDependencies: - '@arethetypeswrong/core' @@ -14673,7 +14674,7 @@ snapshots: '@libsql/isomorphic-ws@0.1.5(bufferutil@4.1.0)(utf-8-validate@6.0.6)': dependencies: - '@types/ws': 8.18.1 + '@types/ws': 8.18.2 ws: 8.22.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil @@ -14973,22 +14974,22 @@ snapshots: dependencies: cross-spawn: 7.0.6 - '@opencode/client@2.0.23(patch_hash=f725d6e6d9af0d56dc6ed71b863518fbfc751af1773cff2311209d2c404cf4cd)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))': + '@opencode/client@2.0.23(patch_hash=f725d6e6d9af0d56dc6ed71b863518fbfc751af1773cff2311209d2c404cf4cd)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))': dependencies: '@opencode/protocol': 2.0.23(patch_hash=17a82f6c2c98fd0371e786c211a4fbdaaba403577a25733e5e99a0c25a16a685) '@opencode/schema': 2.0.23(patch_hash=650f1b8602789407c010861284272dcb6bd6de01ec6a75bf3c0904a0e7596d59) optionalDependencies: - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) '@opencode/protocol@2.0.23(patch_hash=17a82f6c2c98fd0371e786c211a4fbdaaba403577a25733e5e99a0c25a16a685)': dependencies: '@opencode/schema': 2.0.23(patch_hash=650f1b8602789407c010861284272dcb6bd6de01ec6a75bf3c0904a0e7596d59) - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) '@opencode/schema@2.0.23(patch_hash=650f1b8602789407c010861284272dcb6bd6de01ec6a75bf3c0904a0e7596d59)': dependencies: '@standard-schema/spec': 1.1.0 - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) '@oslojs/encoding@1.1.0': {} @@ -16661,7 +16662,7 @@ snapshots: '@types/webidl-conversions': 7.0.3 optional: true - '@types/ws@8.18.1': + '@types/ws@8.18.2': dependencies: '@types/node': 24.12.4 @@ -17140,37 +17141,37 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - alchemy@2.0.0-beta.80(patch_hash=bea1b6c0c0b23157fd5f439a1d7feff430f28ac1621ba135d8a13eeec60ccacd)(d02b4e60a6a3c6b542dfa700209cf5ce): + alchemy@2.0.0-beta.80(patch_hash=bea1b6c0c0b23157fd5f439a1d7feff430f28ac1621ba135d8a13eeec60ccacd)(ba3b2ceb33761bc36b3beb11002eb316): dependencies: - '@alchemy.run/cloudflare-runtime': 2.0.0-beta.80(@distilled.cloud/cloudflare@1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)))(@effect/platform-node@4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6))(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(rolldown@1.2.5)(typescript@7.0.2) - '@alchemy.run/floci': 2.0.0-beta.80(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) + '@alchemy.run/cloudflare-runtime': 2.0.0-beta.80(@distilled.cloud/cloudflare@1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)))(@effect/platform-node@4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6))(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(rolldown@1.2.5)(typescript@7.0.2) + '@alchemy.run/floci': 2.0.0-beta.80(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) '@alchemy.run/node-utils': 2.0.0-beta.80 '@alchemy.run/sigil': 0.1.0-alpha.1(@types/react@19.3.0)(react-devtools-core@6.1.5(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@distilled.cloud/acme': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@distilled.cloud/aws': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@distilled.cloud/axiom': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@distilled.cloud/cloudflare': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@distilled.cloud/doppler': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@distilled.cloud/fly-io': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@distilled.cloud/gcp': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@distilled.cloud/hetzner': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@distilled.cloud/infisical': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@distilled.cloud/neon': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@distilled.cloud/planetscale': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@distilled.cloud/prisma': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@distilled.cloud/railway': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@distilled.cloud/stripe': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@distilled.cloud/zerossl': 1.0.0-rc.13(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@effect/sql-d1': 4.0.0(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@effect/sql-sqlite-do': 4.0.1(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) + '@distilled.cloud/acme': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@distilled.cloud/aws': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@distilled.cloud/axiom': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@distilled.cloud/cloudflare': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@distilled.cloud/core': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@distilled.cloud/doppler': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@distilled.cloud/fly-io': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@distilled.cloud/gcp': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@distilled.cloud/hetzner': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@distilled.cloud/infisical': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@distilled.cloud/neon': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@distilled.cloud/planetscale': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@distilled.cloud/prisma': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@distilled.cloud/railway': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@distilled.cloud/stripe': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@distilled.cloud/zerossl': 1.0.0-rc.13(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@effect/sql-d1': 4.0.0(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@effect/sql-sqlite-do': 4.0.2(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) '@libsql/client': 0.17.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) '@neon/functions': 0.11.0(hono@4.13.7) '@octokit/rest': 22.0.1 '@octokit/webhooks': 14.2.0 '@types/aws-lambda': 8.10.161 capnweb: 0.12.0 - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) fflate: 0.8.3 libsodium-wrappers: 0.8.4 pathe: 2.0.3 @@ -17179,11 +17180,11 @@ snapshots: tinyglobby: 0.2.17 yaml: 2.9.0 optionalDependencies: - '@effect/platform-node': 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6) - '@effect/sql-pg': 4.0.1(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@effect/vitest': 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + '@effect/platform-node': 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) + '@effect/sql-pg': 4.0.2(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@effect/vitest': 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) drizzle-kit: 1.0.0-rc.5-ab785fc - drizzle-orm: 1.0.0-rc.5-ab785fc(74e00612579436038985093f7f46ef5c) + drizzle-orm: 1.0.0-rc.5-ab785fc(da2d75dec181fbafaeb6986c8741457b) mongodb: 6.21.0(@aws-sdk/credential-providers@3.1062.0)(socks@2.8.9) pg: 8.23.0 vite: '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' @@ -18338,17 +18339,17 @@ snapshots: get-tsconfig: 4.14.3 jiti: 2.7.0 - drizzle-orm@1.0.0-rc.5-ab785fc(74e00612579436038985093f7f46ef5c): + drizzle-orm@1.0.0-rc.5-ab785fc(da2d75dec181fbafaeb6986c8741457b): optionalDependencies: '@cloudflare/workers-types': 4.20260604.1 - '@effect/sql-d1': 4.0.0(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@effect/sql-pg': 4.0.1(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@effect/sql-sqlite-bun': 4.0.0-rc.112(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) - '@effect/sql-sqlite-do': 4.0.1(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)) + '@effect/sql-d1': 4.0.0(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@effect/sql-pg': 4.0.2(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@effect/sql-sqlite-bun': 4.0.0-rc.112(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) + '@effect/sql-sqlite-do': 4.0.2(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)) '@electric-sql/pglite': 0.3.15 '@libsql/client': 0.17.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) bun-types: 1.3.14 - effect: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f) + effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) expo-sqlite: 58.0.8(expo@58.0.2)(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) pg: 8.23.0 valibot: 1.2.0(typescript@7.0.2) @@ -18370,7 +18371,7 @@ snapshots: ee-first@1.1.1: {} - effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f): {} + effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688): {} ejs@3.1.10: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index fe0ca51174b3..22e0b07249eb 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -30,14 +30,14 @@ catalog: "@clerk/expo": 4.6.8 "@clerk/react": 6.16.1 "@clerk/shared": 4.33.0 - "@effect/atom-react": 4.0.1 - "@effect/openapi-generator": 4.0.1 - "@effect/platform-node": 4.0.1 - "@effect/platform-node-shared": 4.0.1 - "@effect/sql-pg": 4.0.1 - "@effect/sql-sqlite-do": 4.0.1 + "@effect/atom-react": 4.0.2 + "@effect/openapi-generator": 4.0.2 + "@effect/platform-node": 4.0.2 + "@effect/platform-node-shared": 4.0.2 + "@effect/sql-pg": 4.0.2 + "@effect/sql-sqlite-do": 4.0.2 "@effect/tsgo": 0.46.1 - "@effect/vitest": 4.0.1 + "@effect/vitest": 4.0.2 "@legendapp/list": 3.3.5 "@noble/curves": 1.9.1 "@noble/hashes": 1.8.0 @@ -46,7 +46,8 @@ catalog: "@tailwindcss/oxide": 4.3.3 "@tailwindcss/vite": 4.3.3 "@types/node": 24.12.4 - effect: 4.0.1 + "@types/ws": 8.18.2 + effect: 4.0.2 jose: 6.2.2 lightningcss: 1.33.0 tailwindcss: 4.3.3 @@ -86,15 +87,15 @@ minimumReleaseAgeExclude: - "@distilled.cloud/core@1.0.0-rc.13" - "@distilled.cloud/neon@1.0.0-rc.13" - "@distilled.cloud/planetscale@1.0.0-rc.13" - - "@effect/atom-react@4.0.1" - - "@effect/openapi-generator@4.0.1" - - "@effect/platform-node-shared@4.0.1" - - "@effect/platform-node@4.0.1" - - "@effect/sql-pg@4.0.1" - - "@effect/sql-sqlite-do@4.0.1" - - "@effect/vitest@4.0.1" + - "@effect/atom-react@4.0.2" + - "@effect/openapi-generator@4.0.2" + - "@effect/platform-node-shared@4.0.2" + - "@effect/platform-node@4.0.2" + - "@effect/sql-pg@4.0.2" + - "@effect/sql-sqlite-do@4.0.2" + - "@effect/vitest@4.0.2" - alchemy@2.0.0-beta.80 - - effect@4.0.1 + - effect@4.0.2 - "@legendapp/list@3.3.5" - "@expo/cli@58.1.1" - "@expo/env@2.5.1" @@ -252,6 +253,7 @@ overrides: tailwindcss: "catalog:" # Ranges from @effect/platform-node, jsdom, unifont, Expo and libsql otherwise # settle on two patch releases of each major. Older majors stay as they are. + "@types/ws@^8": "catalog:" "undici@^8": "catalog:" "ws@^8": "catalog:" vite: "catalog:" @@ -274,7 +276,7 @@ patchedDependencies: expo-blur@58.0.3: patches/expo-blur@58.0.3.patch expo-audio@58.0.4: patches/expo-audio@58.0.4.patch "@clerk/expo@4.6.8": patches/@clerk__expo@4.6.8.patch - "@effect/vitest@4.0.1": patches/@effect__vitest@4.0.1.patch + "@effect/vitest@4.0.2": patches/@effect__vitest@4.0.2.patch "@expo/metro-config@58.0.6": patches/@expo__metro-config@58.0.6.patch expo-sharing@58.0.13: patches/expo-sharing@58.0.13.patch expo-widgets@58.0.11: patches/expo-widgets@58.0.11.patch @@ -290,7 +292,7 @@ patchedDependencies: "@react-native-menu/menu@2.0.0": patches/@react-native-menu__menu@2.0.0.patch "@react-navigation/native-stack@7.17.6": patches/@react-navigation%2Fnative-stack@7.17.6.patch dbus-next@0.10.2: patches/dbus-next@0.10.2.patch - effect@4.0.1: patches/effect@4.0.1.patch + effect@4.0.2: patches/effect@4.0.2.patch node-pty@1.2.0-beta.15: patches/node-pty@1.2.0-beta.15.patch # Link http(s) URLs with a port or a single-label host. md4c otherwise requires a dotted host and stops at ':'. react-native-nitro-markdown@0.5.8: patches/react-native-nitro-markdown@0.5.8.patch From ecfb7342edde9160b7df4231d4f145c7e4a30530 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 12:18:27 -0700 Subject: [PATCH 02/39] fix(devices): recover stalled video without losing simulator input (#17566) --- .../devices/device-stream.browser.test.ts | 3 + apps/server/src/device/DeviceToolchain.ts | 4 +- .../device/DeviceStreamView.test.tsx | 103 +++++++++++++++- .../components/device/DeviceStreamView.tsx | 26 ++-- .../client-runtime/src/device/stream.test.ts | 114 +++++++++++++++++- packages/client-runtime/src/device/stream.ts | 53 ++++++-- 6 files changed, 272 insertions(+), 31 deletions(-) diff --git a/apps/mobile/src/features/devices/device-stream.browser.test.ts b/apps/mobile/src/features/devices/device-stream.browser.test.ts index 5aeefdac0203..24766242b37e 100644 --- a/apps/mobile/src/features/devices/device-stream.browser.test.ts +++ b/apps/mobile/src/features/devices/device-stream.browser.test.ts @@ -38,6 +38,7 @@ async function setup() { static OPEN = 1; readyState = 1; onopen: (() => void) | null = null; + onmessage: ((event: { data: ArrayBuffer }) => void) | null = null; close = vi.fn(); send = vi.fn(); constructor() { @@ -86,6 +87,8 @@ it("bridges shared first-frame readiness, image failure, and a successful fresh const { elements, messages, sockets, configuration } = await setup(); sockets[0]!.onopen?.(); expect(messages()).not.toContainEqual({ type: "status", status: "streaming" }); + expect(messages()).not.toContainEqual({ type: "input", connected: true }); + sockets[0]!.onmessage?.({ data: new Uint8Array([0x83]).buffer }); expect(messages()).toContainEqual({ type: "input", connected: true }); const image = elements.find((element) => element.tag === "img")!; image.naturalWidth = 400; diff --git a/apps/server/src/device/DeviceToolchain.ts b/apps/server/src/device/DeviceToolchain.ts index f30960ed2212..af590787e4ee 100644 --- a/apps/server/src/device/DeviceToolchain.ts +++ b/apps/server/src/device/DeviceToolchain.ts @@ -26,9 +26,9 @@ import * as Semaphore from "effect/Semaphore"; import * as ProcessRunner from "../processRunner.ts"; const DEVICE_HUB_PACKAGE = "expo-device-hub"; -export const DEVICE_HUB_VERSION = "0.12.0"; +export const DEVICE_HUB_VERSION = "0.15.3"; const AGENT_DEVICE_PACKAGE = "agent-device"; -export const AGENT_DEVICE_VERSION = "0.21.12"; +export const AGENT_DEVICE_VERSION = "0.21.23"; const INSTALL_TIMEOUT = Duration.minutes(10); const installLock = Semaphore.makeUnsafe(1); diff --git a/apps/web/src/components/device/DeviceStreamView.test.tsx b/apps/web/src/components/device/DeviceStreamView.test.tsx index 7e0392340129..a7f4079b5683 100644 --- a/apps/web/src/components/device/DeviceStreamView.test.tsx +++ b/apps/web/src/components/device/DeviceStreamView.test.tsx @@ -1,4 +1,4 @@ -import { act, useSyncExternalStore } from "react"; +import { act, useEffect, useSyncExternalStore } from "react"; import { create, type ReactTestRenderer } from "react-test-renderer"; import { EnvironmentId } from "@t3tools/contracts"; import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test"; @@ -20,6 +20,20 @@ vi.mock("~/state/device", () => ({ useDeviceHubAccess: () => useSyncExternalStore(accessStore.subscribe, () => accessStore.value), refreshDeviceHubAccess: () => accessStore.refresh(), })); +// Replace GPU allocation while keeping the real React and stream lifecycles. +let viewerMounts = 0; +let viewerUnmounts = 0; +vi.mock("./DevicePhoneViewport", () => ({ + DevicePhoneViewport: function Viewer() { + useEffect(() => { + viewerMounts++; + return () => { + viewerUnmounts++; + }; + }, []); + return null; + }, +})); import { DeviceStreamView } from "./DeviceStreamView"; class Image extends EventTarget { @@ -34,6 +48,8 @@ let renderer: ReactTestRenderer | undefined; let primes = 0; beforeEach(() => { primes = 0; + viewerMounts = 0; + viewerUnmounts = 0; }); afterEach(async () => { await act(async () => renderer?.unmount()); @@ -42,18 +58,57 @@ afterEach(async () => { vi.unstubAllGlobals(); }); -async function setup() { +async function setup(h264 = false) { vi.useFakeTimers(); + vi.stubGlobal("window", globalThis); vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); - vi.stubGlobal("fetch", () => { + let videoBody: ReadableStreamDefaultController | undefined; + let output: VideoFrameOutputCallback | undefined; + if (h264) { + vi.stubGlobal( + "VideoDecoder", + class { + static isConfigSupported = async () => ({ supported: true }); + state = "unconfigured"; + constructor(callbacks: VideoDecoderInit) { + output = callbacks.output; + } + configure() { + this.state = "configured"; + } + close() { + this.state = "closed"; + } + }, + ); + vi.stubGlobal("EncodedVideoChunk", vi.fn()); + } + vi.stubGlobal("fetch", (url: string, init: RequestInit) => { + if (url.endsWith("stream.avcc")) { + return Promise.resolve( + new Response( + new ReadableStream({ + start(controller) { + videoBody = controller; + init.signal?.addEventListener("abort", () => controller.error(new Error("aborted"))); + }, + }), + ), + ); + } primes++; return Promise.resolve(new Response("prime")); }); + const sockets: Array<{ onmessage?: (event: { data: ArrayBuffer }) => void }> = []; vi.stubGlobal( "WebSocket", class { static OPEN = 1; readyState = 1; + constructor() { + sockets.push(this); + } + onmessage?: (event: { data: ArrayBuffer }) => void; send() {} close() {} }, @@ -73,6 +128,7 @@ async function setup() { deviceId="test" platform="ios" visible={visible} + allowPhoneView={h264} /> ); await act(async () => { @@ -84,13 +140,30 @@ async function setup() { return image; } return { + getContext: () => ({ drawImage() {} }), style: { setProperty() {} }, getBoundingClientRect: () => ({ width: 400, height: 800 }), }; }, }); }); - return { images, view }; + return { + images, + view, + configure() { + const json = new TextEncoder().encode( + JSON.stringify({ width: 400, height: 800, orientation: "portrait" }), + ); + const packet = new Uint8Array(1 + json.length); + packet[0] = 0x82; + packet.set(json, 1); + sockets[0]?.onmessage?.({ data: packet.buffer }); + videoBody?.enqueue(new Uint8Array([0, 0, 0, 5, 1, 1, 0x64, 0, 0x1f])); + }, + frame() { + output?.({ displayWidth: 400, displayHeight: 800, close() {} } as VideoFrame); + }, + }; } it("removes MJPEG requests while hidden and reconnects when shown", async () => { @@ -135,3 +208,25 @@ it("starts exactly one new stream per Reconnect press", async () => { await act(async () => renderer!.root.findByType("button").props.onClick()); expect(primes).toBe(2); }); + +it("keeps the 3D viewer mounted across iOS video recovery and releases it when hidden", async () => { + const { configure, frame, view } = await setup(true); + await act(async () => configure()); + expect(viewerMounts).toBe(0); + await act(async () => frame()); + expect(viewerMounts).toBe(1); + await act(async () => { + await vi.advanceTimersByTimeAsync(15_000); + }); + expect(viewerUnmounts).toBe(0); + expect(viewerMounts).toBe(1); + await act(async () => { + await vi.advanceTimersByTimeAsync(1_000); + configure(); + }); + await act(async () => frame()); + expect(viewerMounts).toBe(1); + await act(async () => renderer!.update(view(false))); + expect(viewerUnmounts).toBe(1); + expect(vi.getTimerCount()).toBe(0); +}); diff --git a/apps/web/src/components/device/DeviceStreamView.tsx b/apps/web/src/components/device/DeviceStreamView.tsx index 25c4c39dae2f..8db311f2fe14 100644 --- a/apps/web/src/components/device/DeviceStreamView.tsx +++ b/apps/web/src/components/device/DeviceStreamView.tsx @@ -94,6 +94,7 @@ export function DeviceStreamView(props: { const canvasRef = useRef(null); const clientRef = useRef(null); const [status, setStatus] = useState("connecting"); + const [hasFrame, setHasFrame] = useState(false); const [detail, setDetail] = useState(undefined); const [showRestartNotice, setShowRestartNotice] = useState(false); const [screen, setScreen] = useState(null); @@ -123,6 +124,7 @@ export function DeviceStreamView(props: { onDuoUnavailable: onPhoneUnavailable, onStatus: (next, nextDetail) => { setStatus(next); + if (next === "streaming") setHasFrame(true); setDetail(nextDetail); if (next !== "connecting") setShowRestartNotice(false); }, @@ -150,6 +152,8 @@ export function DeviceStreamView(props: { }, ); clientRef.current = client; + setPhoneUnavailable(false); + setHasFrame(false); setMjpegUrl(null); setInputState({ connected: false }); client.start(); @@ -188,16 +192,13 @@ export function DeviceStreamView(props: { return w / h; }, [props.platform, screen]); - // Android restarts its encoder when a fold changes the framebuffer size. - // Keep the last decoded frame and viewer mounted while the next keyframe arrives. - const retainingAndroidFrame = - props.platform === "android" && - status === "connecting" && - inputState.connected && - screen !== null; + // Encoder restarts and video reconnects retain the decoded frame and viewer + // while input is still connected and the next keyframe is on its way. + const retainingFrame = + status === "connecting" && hasFrame && inputState.connected && screen !== null; const showPhone = props.allowPhoneView && - (status === "streaming" || retainingAndroidFrame) && + (status === "streaming" || retainingFrame) && props.visible && presentation === "phone" && !phoneUnavailable && @@ -205,10 +206,10 @@ export function DeviceStreamView(props: { !props.axOverlay && (!isDuo || screen?.supportsHingeAngle === true); useEffect(() => { - if (!retainingAndroidFrame || !showPhone) return; + if (!retainingFrame || !showPhone) return; const timeout = window.setTimeout(() => setShowRestartNotice(true), 2_000); return () => window.clearTimeout(timeout); - }, [retainingAndroidFrame, showPhone]); + }, [retainingFrame, showPhone]); const controlsInset = props.renderControls && !showPhone ? CONTROLS_RAIL_WIDTH : 0; // The frame is the largest box at `aspect` that fits the container, so a @@ -532,14 +533,14 @@ export function DeviceStreamView(props: { ) : null} - {retainingAndroidFrame && showPhone && showRestartNotice ? ( + {retainingFrame && showPhone && showRestartNotice ? (
Waiting for device video…
) : null} - {status !== "streaming" && !(retainingAndroidFrame && showPhone) ? ( + {status !== "streaming" && !(retainingFrame && showPhone) ? (
diff --git a/packages/client-runtime/src/device/stream.test.ts b/packages/client-runtime/src/device/stream.test.ts index 7ea5aca27abf..f29484eb00b4 100644 --- a/packages/client-runtime/src/device/stream.test.ts +++ b/packages/client-runtime/src/device/stream.test.ts @@ -385,10 +385,14 @@ function recoveryFixture(platform: "ios" | "android" = "ios", preferMjpeg = true configure() { this.state = "configured"; } + reset = vi.fn(() => { + this.state = "unconfigured"; + this.decodeQueueSize = 0; + }); close() { this.state = "closed"; } - decode() {} + decode = vi.fn(); readonly callbacks: { output: (frame: VideoFrame) => void; error: () => void }; constructor(callbacks: { output: (frame: VideoFrame) => void; error: () => void }) { this.callbacks = callbacks; @@ -480,6 +484,93 @@ describe("shared device stream readiness and recovery", () => { vi.unstubAllGlobals(); }); + it("waits for iOS input admission and respects native input loss and recovery", async () => { + const { client, sockets, events } = recoveryFixture(); + client.start(); + await vi.advanceTimersByTimeAsync(0); + const socket = sockets[0]!; + socket.onopen?.(); + expect(events.onInputConnected).not.toHaveBeenCalledWith(true); + socket.onmessage?.({ data: new Uint8Array([0x83]).buffer }); + expect(events.onInputConnected).toHaveBeenLastCalledWith(true); + const config = (inputUnavailable: boolean) => { + const json = new TextEncoder().encode( + JSON.stringify({ width: 400, height: 800, orientation: "portrait", inputUnavailable }), + ); + const packet = new Uint8Array(1 + json.length); + packet[0] = 0x82; + packet.set(json, 1); + socket.onmessage?.({ data: packet.buffer }); + }; + config(true); + expect(events.onInputConnected).toHaveBeenLastCalledWith( + false, + "Simulator input is unavailable", + ); + socket.send.mockClear(); + client.sendTouch("begin", 0.2, 0.3); + client.pressButton("home"); + expect(socket.send).not.toHaveBeenCalled(); + config(false); + expect(events.onInputConnected).toHaveBeenLastCalledWith(true, undefined); + client.sendTouch("begin", 0.2, 0.3); + expect(socket.send).toHaveBeenCalledOnce(); + client.stop(); + expect(vi.getTimerCount()).toBe(0); + }); + + it("drops an iOS decode backlog and resumes at a keyframe without switching to MJPEG", async () => { + const { client, videoBodies, decoders, events, decodedFrame } = recoveryFixture("ios", false); + client.start(); + await vi.advanceTimersByTimeAsync(0); + const body = videoBodies[0]!; + body.enqueue(new Uint8Array([...envelope(1, [1, 0x64, 0, 0x1f]), ...envelope(2, [1])])); + await vi.advanceTimersByTimeAsync(0); + decodedFrame(); + const decoder = decoders[0]!; + expect(decoder.decode).toHaveBeenCalledOnce(); + decoder.decodeQueueSize = 9; + body.enqueue(new Uint8Array([...envelope(3, [2]), ...envelope(3, [3])])); + await vi.advanceTimersByTimeAsync(0); + expect(decoder.reset).toHaveBeenCalledOnce(); + expect(decoder.decode).toHaveBeenCalledOnce(); + body.enqueue(new Uint8Array([...envelope(2, [4]), ...envelope(3, [5])])); + await vi.advanceTimersByTimeAsync(0); + expect(decoder.decode).toHaveBeenCalledTimes(3); + expect(events.onMjpegFallback).not.toHaveBeenCalled(); + expect(events.onStatus).toHaveBeenLastCalledWith("streaming", undefined); + client.stop(); + }); + + it("does not repaint a delayed JPEG seed over newer decoded video", async () => { + const { client, videoBodies, drawImage, decodedFrame } = recoveryFixture("ios", false); + let resolveBitmap!: (bitmap: ImageBitmap) => void; + vi.stubGlobal( + "createImageBitmap", + () => + new Promise((resolve) => { + resolveBitmap = resolve; + }), + ); + client.start(); + await vi.advanceTimersByTimeAsync(0); + videoBodies[0]!.enqueue( + new Uint8Array([ + ...envelope(4, [1]), + ...envelope(1, [1, 0x64, 0, 0x1f]), + ...envelope(2, [2]), + ]), + ); + await vi.advanceTimersByTimeAsync(0); + decodedFrame(); + const close = vi.fn(); + resolveBitmap({ width: 400, height: 800, close } as unknown as ImageBitmap); + await vi.advanceTimersByTimeAsync(0); + expect(drawImage).toHaveBeenCalledOnce(); + expect(close).toHaveBeenCalledOnce(); + client.stop(); + }); + it("waits for actual MJPEG dimensions without requiring a multipart load event", async () => { const { client, events, image } = recoveryFixture(); client.start(); @@ -713,22 +804,35 @@ describe("shared device stream readiness and recovery", () => { }); }); -it("reports a stalled AVCC body after its initial image instead of leaving a frozen streaming state", async () => { - const { client, videoBodies, events, signals } = recoveryFixture("ios", false); +it("reopens a stalled AVCC body without disconnecting input, then receives fresh video", async () => { + const { client, videoBodies, events, signals, sockets, decodedFrame } = recoveryFixture( + "ios", + false, + ); const close = vi.fn(); vi.stubGlobal("createImageBitmap", () => Promise.resolve({ width: 400, height: 800, close })); try { client.start(); await vi.advanceTimersByTimeAsync(0); + sockets[0]!.onmessage?.({ data: new Uint8Array([0x83]).buffer }); videoBodies[0]!.enqueue(new Uint8Array(envelope(4, [1]))); await vi.advanceTimersByTimeAsync(0); expect(events.onStatus).toHaveBeenLastCalledWith("streaming", undefined); await vi.advanceTimersByTimeAsync(15_000); expect(events.onStatus).toHaveBeenLastCalledWith( - "error", + "connecting", expect.stringContaining("stopped receiving video"), ); - expect(signals.every((signal) => signal.aborted)).toBe(true); + expect(signals[1]!.aborted).toBe(true); + expect(sockets[0]!.close).not.toHaveBeenCalled(); + expect(events.onInputConnected).toHaveBeenLastCalledWith(true); + await vi.advanceTimersByTimeAsync(1_000); + expect(videoBodies).toHaveLength(2); + videoBodies[1]!.enqueue(new Uint8Array(envelope(1, [1, 0x64, 0, 0x1f]))); + await vi.advanceTimersByTimeAsync(0); + decodedFrame(); + expect(events.onStatus).toHaveBeenLastCalledWith("streaming", undefined); + client.stop(); expect(vi.getTimerCount()).toBe(0); } finally { client.stop(); diff --git a/packages/client-runtime/src/device/stream.ts b/packages/client-runtime/src/device/stream.ts index 6b1ff6158879..27ab7c93b416 100644 --- a/packages/client-runtime/src/device/stream.ts +++ b/packages/client-runtime/src/device/stream.ts @@ -55,6 +55,7 @@ const screenConfigSchema = Schema.Struct({ "landscape_left", "landscape_right", ]), + inputUnavailable: Schema.optionalKey(Schema.Boolean), screenId: Schema.optionalKey(Schema.Number), supportsHingeAngle: Schema.optionalKey(Schema.Boolean), supportsPhysicalOrientation: Schema.optionalKey(Schema.Boolean), @@ -130,6 +131,7 @@ const IOS_MSG_ORIENTATION = 0x07; const IOS_MSG_HARDWARE_KEYBOARD = 0x0d; // helper -> browser. const IOS_TAG_SCREEN_CONFIG = 0x82; +const IOS_TAG_INPUT_ADMITTED = 0x83; const encoder = new TextEncoder(); const decoder = new TextDecoder(); @@ -364,6 +366,9 @@ export function createDeviceStreamClient( const retryTimers = new Map<"video" | "input", ReturnType>(); let primeController: AbortController | null = null; let videoDecoder: VideoDecoder | null = null; + let decoderConfig: VideoDecoderConfig | null = null; + let decodedVideoGeneration: number | null = null; + let iosInputUnavailable = false; let timestamp = 0; let awaitingKeyframe = true; let screen: DeviceScreenSize | null = null; @@ -517,6 +522,7 @@ export function createDeviceStreamClient( // Already closed. } videoDecoder = null; + decoderConfig = null; awaitingKeyframe = true; }; @@ -537,8 +543,10 @@ export function createDeviceStreamClient( if ( videoDecoder === decoder && (platform !== "ios" || feedGeneration === videoGeneration) - ) + ) { + decodedVideoGeneration = feedGeneration; paint(frame, frame.displayWidth, frame.displayHeight); + } } finally { frame.close(); } @@ -572,6 +580,7 @@ export function createDeviceStreamClient( try { if (!videoDecoder || videoDecoder.state === "closed") videoDecoder = makeDecoder(); videoDecoder.configure(full); + decoderConfig = full; return true; } catch (cause) { if (platform === "android") fail(`Video decoder: ${(cause as Error).message}`); @@ -586,8 +595,22 @@ export function createDeviceStreamClient( awaitingKeyframe = false; } if (videoDecoder.decodeQueueSize > SOFT_DECODE_QUEUE) { - recoverDecoder(); - return; + if (platform !== "ios" || !decoderConfig) { + recoverDecoder(); + return; + } + // A slow viewer can accumulate healthy H.264 frames. Drop that backlog + // and resume at an IDR instead of permanently giving up its 3D view. + try { + videoDecoder.reset(); + videoDecoder.configure(decoderConfig); + awaitingKeyframe = true; + if (!isKey) return; + awaitingKeyframe = false; + } catch { + recoverDecoder(); + return; + } } try { videoDecoder.decode( @@ -659,7 +682,9 @@ export function createDeviceStreamClient( for (;;) { // An AVCC body can stay open after its helper stops producing frames. const timer = setTimeout(() => { - if (isCurrent()) fail("Device stream stopped receiving video. Reconnect to try again."); + if (!isCurrent()) return; + retryDetail = "Device stream stopped receiving video. Reconnecting…"; + videoController.abort(); }, FIRST_FRAME_TIMEOUT_MS); videoReadTimer = timer; let result: ReadableStreamReadResult; @@ -678,7 +703,8 @@ export function createDeviceStreamClient( void createImageBitmap(new Blob([chunk.payload as BlobPart], { type: "image/jpeg" })) .then((bitmap) => { try { - if (isCurrent()) paint(bitmap, bitmap.width, bitmap.height); + if (isCurrent() && decodedVideoGeneration !== feedGeneration) + paint(bitmap, bitmap.width, bitmap.height); } finally { bitmap.close(); } @@ -714,7 +740,7 @@ export function createDeviceStreamClient( } } catch (cause) { if (!isCurrent()) return; - retryDetail = (cause as Error).message; + retryDetail ??= (cause as Error).message; } if (isCurrent()) { controller = null; @@ -799,13 +825,17 @@ export function createDeviceStreamClient( ws.onopen = () => { if (stopped || socket !== ws) return; ws.send(taggedJson(IOS_MSG_HARDWARE_KEYBOARD, { enabled: false })); - events.onInputConnected(true); }; ws.onmessage = (event) => { if (stopped || socket !== ws) return; if (!(event.data instanceof ArrayBuffer)) return; const bytes = new Uint8Array(event.data); if (socket !== ws || stopped || bytes.length < 1) return; + if (bytes[0] === IOS_TAG_INPUT_ADMITTED) { + iosInputUnavailable = false; + events.onInputConnected(true); + return; + } try { const payload: unknown = JSON.parse(decoder.decode(bytes.subarray(1))); if (bytes[0] === 0x90) { @@ -814,6 +844,11 @@ export function createDeviceStreamClient( } else if (bytes[0] === IOS_TAG_SCREEN_CONFIG) { const config = decodeScreenConfig(payload); if (Option.isSome(config)) { + iosInputUnavailable = config.value.inputUnavailable === true; + events.onInputConnected( + !iosInputUnavailable, + iosInputUnavailable ? "Simulator input is unavailable" : undefined, + ); const previous = screen; screen = config.value; if (screen.hingePose && screen.hingePose !== previous?.hingePose) @@ -944,6 +979,7 @@ export function createDeviceStreamClient( stopped = false; generation++; configuring = false; + iosInputUnavailable = false; connecting(); if (platform === "ios") { if (!target.videoOnly) void connectIosInput(); @@ -986,7 +1022,8 @@ export function createDeviceStreamClient( }; const send = (payload: Uint8Array | string) => { - if (!stopped && socket?.readyState === WebSocket.OPEN) socket.send(payload); + if (!stopped && !iosInputUnavailable && socket?.readyState === WebSocket.OPEN) + socket.send(payload); }; const rawPoint = (x: number, y: number) => { From 7856908b8492b7a5c381aa2d5a5c4d4292eb976b Mon Sep 17 00:00:00 2001 From: maria Date: Fri, 9 Oct 2026 16:21:27 -0300 Subject: [PATCH 03/39] fix(web): keep checkout stable while pr actions load (#16625) Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com> --- .../src/components/pullRequest/PullRequestDetailPanel.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx index 1466e726b60f..1a36fb94a37b 100644 --- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx +++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx @@ -1648,10 +1648,10 @@ export function PullRequestDetailPanel({ actions={ handoffSummary ? ( - {checkoutControl} {handoffSummary.state === "open" && handoffSummary.mergeability === "conflicting" ? resolveConflictsControl : null} + {checkoutControl} ) : undefined } @@ -1864,7 +1864,6 @@ export function PullRequestDetailPanel({ threadRef={null} /> ) : null} - {checkoutControl} {/* Said where the Merge button is, because it is the answer to why nobody has pressed it: the merge is already asked for, and the host is holding it. */} {autoMergeArmed && primaryAction !== "auto-merge-armed" ? ( @@ -1997,6 +1996,9 @@ export function PullRequestDetailPanel({ ) : null} + {/* Keep checkout beside the menu so host actions arriving on its left cannot + move a different action under the reader's pointer. */} + {checkoutControl} Date: Fri, 9 Oct 2026 16:21:34 -0300 Subject: [PATCH 04/39] fix(mobile): show waiting thread status (#16693) Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com> --- apps/mobile/src/features/threads/thread-list-v2-items.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/mobile/src/features/threads/thread-list-v2-items.tsx b/apps/mobile/src/features/threads/thread-list-v2-items.tsx index 88ad0d43ea65..d7596305cff2 100644 --- a/apps/mobile/src/features/threads/thread-list-v2-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-v2-items.tsx @@ -90,6 +90,12 @@ const STATUS_LABEL_BY_STATUS: Partial> = className: "text-adaptive-sky-600-400", iconTintClassName: "accent-adaptive-sky-600-400", }, + waiting: { + label: "Waiting", + icon: "clock", + className: "text-foreground-muted", + iconTintClassName: "accent-foreground-muted", + }, failed: { label: "Failed", icon: "exclamationmark.circle", From 5722496b79079ac317a984f3baca8c88f41af070 Mon Sep 17 00:00:00 2001 From: maria Date: Fri, 9 Oct 2026 16:22:15 -0300 Subject: [PATCH 05/39] feat(web): add parent thread breadcrumb navigation (#16666) Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com> --- apps/web/src/components/ChatView.tsx | 2 + apps/web/src/components/chat/ChatHeader.tsx | 81 +++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 214b0605c833..dbee50a28ac7 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -11307,6 +11307,8 @@ export default function ChatView(props: ChatViewProps) { isServerThread={isServerThread} activeThreadTitle={activeThread.title} activeProject={activeProject ?? null} + parentThreadLink={parentThreadLink} + onOpenThread={onOpenRelatedThread} rightPanelOpen={inlineRightPanelOwnsTitleBar} onNewThreadInProject={handleNewThreadInActiveProject} {...(activeDraftLogicalProjectKey diff --git a/apps/web/src/components/chat/ChatHeader.tsx b/apps/web/src/components/chat/ChatHeader.tsx index 51c7ebb15f47..b50a40ecfb91 100644 --- a/apps/web/src/components/chat/ChatHeader.tsx +++ b/apps/web/src/components/chat/ChatHeader.tsx @@ -37,6 +37,7 @@ import { WorkspaceBreadcrumbText, } from "../WorkspaceBreadcrumb"; import { cn } from "~/lib/utils"; +import { useClientSettings } from "../../hooks/useSettings"; interface ChatHeaderProps { activeThreadEnvironmentId: EnvironmentId; @@ -45,6 +46,8 @@ interface ChatHeaderProps { /** Drafts have no server thread yet, so the title carries no action menu. */ isServerThread: boolean; activeProject: EnvironmentProject | null; + parentThreadLink: { threadId: ThreadId; title: string } | null; + onOpenThread: (threadId: ThreadId) => void; rightPanelOpen: boolean; onNewThreadInProject: () => void; onOpenProjectSettings?: (() => void) | undefined; @@ -77,12 +80,17 @@ export const ChatHeader = memo(function ChatHeader({ activeThreadTitle, isServerThread, activeProject, + parentThreadLink, + onOpenThread, rightPanelOpen, onNewThreadInProject, onOpenProjectSettings, }: ChatHeaderProps) { const activeProjectName = activeProject?.title; const activeProjectCwd = activeProject?.workspaceRoot ?? null; + const interfaceFont = useClientSettings((settings) => settings.fontFamilySans); + const breadcrumbContainerRef = useRef(null); + const [collapseParentTitle, setCollapseParentTitle] = useState(false); const activeThreadRef = useMemo( () => scopeThreadRef(activeThreadEnvironmentId, activeThreadId), [activeThreadEnvironmentId, activeThreadId], @@ -116,6 +124,44 @@ export const ChatHeader = memo(function ChatHeader({ renaming.environmentId === activeThreadEnvironmentId ? renaming.title : null; + // Leaving rename swaps the input back for the title, which needs a fresh fit. + const isRenamingTitle = renamingTitle !== null; + useEffect(() => { + const list = breadcrumbContainerRef.current?.querySelector("ol"); + if (!list || !parentThreadLink) return; + // Measure the untruncated labels, including a collapsed parent's hidden + // text, so expanding and collapsing never change the fit calculation. + // Width-capped labels (the project) never grow, so their overflow is skipped. + const measure = () => { + const gap = Number.parseFloat(getComputedStyle(list).columnGap) || 0; + const width = Array.from(list.children).reduce( + (total, item) => { + const label = item.querySelector('[data-slot="workspace-breadcrumb-text"]'); + const ellipsis = item.querySelector("[data-parent-breadcrumb-ellipsis]"); + return ( + total + + (item.firstElementChild?.getBoundingClientRect().width ?? 0) + + (label && getComputedStyle(label).maxWidth === "none" + ? label.scrollWidth - label.clientWidth + : 0) - + (ellipsis?.getBoundingClientRect().width ?? 0) + ); + }, + gap * (list.children.length - 1), + ); + setCollapseParentTitle(width > list.clientWidth); + }; + measure(); + const frame = requestAnimationFrame(measure); + document.fonts.addEventListener("loadingdone", measure); + const observer = new ResizeObserver(measure); + observer.observe(list); + return () => { + cancelAnimationFrame(frame); + document.fonts.removeEventListener("loadingdone", measure); + observer.disconnect(); + }; + }, [activeProjectName, activeThreadTitle, parentThreadLink, interfaceFont, isRenamingTitle]); const renameCommittedRef = useRef(false); const startRename = useCallback(() => { if ( @@ -253,6 +299,7 @@ export const ChatHeader = memo(function ChatHeader({ ); return (
) : null} + {parentThreadLink ? ( + <> + + + onOpenThread(parentThreadLink.threadId)} + className="inline-flex min-w-0 max-w-full cursor-pointer items-center rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring" + /> + } + > + + {parentThreadLink.title} + + {collapseParentTitle ? ( + + ... + + ) : null} + + {parentThreadLink.title} + + + + / + + + ) : null} {renamingTitle !== null ? ( Date: Fri, 9 Oct 2026 16:22:38 -0300 Subject: [PATCH 06/39] fix(server): restart inactivity after snoozed threads wake (#16674) Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com> --- .../src/orchestration-v2/Orchestrator.ts | 4 +++- .../src/orchestration-v2/ProjectionStore.ts | 6 +++++- .../ThreadSettlementService.test.ts | 20 +++++++++++++++++++ .../ThreadSettlementService.ts | 10 ++++++++-- .../src/orchestration-v2/runtimeLayer.test.ts | 18 +++++++++++++++++ packages/contracts/src/orchestrationV2.ts | 3 +++ 6 files changed, 57 insertions(+), 4 deletions(-) diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts index 4f870a606e00..977b225f5256 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts @@ -2831,6 +2831,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio ...thread, snoozedUntil: null, snoozedAt: null, + lastSnoozeWakeAt: alreadyAwake ? thread.lastSnoozeWakeAt : now, updatedAt: alreadyAwake ? thread.updatedAt : now, }; } @@ -2862,6 +2863,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio settledAt: thread.settledOverride === "settled" ? null : thread.settledAt, snoozedUntil: null, snoozedAt: null, + lastSnoozeWakeAt: thread.snoozedUntil == null ? thread.lastSnoozeWakeAt : now, updatedAt: alreadyPinned && !promotes ? thread.updatedAt : now, }; } @@ -2933,7 +2935,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio thread.snoozedUntil != null && DateTime.toEpochMillis(thread.snoozedUntil) === Date.parse(thread.limitRecovery.resetAt) - ? { snoozedUntil: null, snoozedAt: null } + ? { snoozedUntil: null, snoozedAt: null, lastSnoozeWakeAt: now } : {}), ...(command.branch === undefined ? {} : { branch: command.branch }), ...(command.worktreePath === undefined ? {} : { worktreePath: command.worktreePath }), diff --git a/apps/server/src/orchestration-v2/ProjectionStore.ts b/apps/server/src/orchestration-v2/ProjectionStore.ts index a6a092ff84b7..8c33faef808b 100644 --- a/apps/server/src/orchestration-v2/ProjectionStore.ts +++ b/apps/server/src/orchestration-v2/ProjectionStore.ts @@ -204,7 +204,10 @@ export type ProjectionSettlementCandidate = Pick< | "activityRunStatus" | "pendingRuntimeRequest" | "pendingBackgroundTasks" -> & { readonly latestUserAuthoredMessageAt: DateTime.Utc | null }; +> & + Pick & { + readonly latestUserAuthoredMessageAt: DateTime.Utc | null; + }; const ProjectionCheckpointContext = Schema.Struct({ runs: Schema.Array( @@ -6035,6 +6038,7 @@ export const layerMemory: Layer.Layer = Layer.effect( const shell = threadShellFromProjection(projection); return { ...shell, + lastSnoozeWakeAt: projection.thread.lastSnoozeWakeAt ?? null, latestUserAuthoredMessageAt: shell.latestUserAuthoredMessageAt ?? null, }; }) diff --git a/apps/server/src/orchestration-v2/ThreadSettlementService.test.ts b/apps/server/src/orchestration-v2/ThreadSettlementService.test.ts index 4b0e451e0ae6..00daff5a8c87 100644 --- a/apps/server/src/orchestration-v2/ThreadSettlementService.test.ts +++ b/apps/server/src/orchestration-v2/ThreadSettlementService.test.ts @@ -276,6 +276,26 @@ describe("resolveAutoSettlementAt", () => { ).toBeNull(); }); + it.each(["manual", "timer"])("restarts inactivity on a %s snooze wake", (wake) => { + const thread = { + ...shell({ latestRunCompletedAt: at(-4 * DAY_MS) }), + ...(wake === "manual" + ? { lastSnoozeWakeAt: at(-1_000) } + : { snoozedAt: at(-3 * DAY_MS), snoozedUntil: at(-1_000) }), + }; + const input = { + thread, + pullRequest: null, + nowMs: NOW_MS, + autoSettleAfterDays: 1, + autoSettleOnMerge: false, + }; + expect(ThreadSettlementService.resolveAutoSettlementAt(input)).toBeNull(); + expect( + ThreadSettlementService.resolveAutoSettlementAt({ ...input, nowMs: NOW_MS + DAY_MS }), + ).toEqual(at(-1_000)); + }); + it("settles on merge only after the user's last action and preserves the activity time", () => { const thread = shell({ latestUserMessageAt: at(-2 * 60 * 60 * 1_000), diff --git a/apps/server/src/orchestration-v2/ThreadSettlementService.ts b/apps/server/src/orchestration-v2/ThreadSettlementService.ts index 5e3f7e795600..8b55e25cdc39 100644 --- a/apps/server/src/orchestration-v2/ThreadSettlementService.ts +++ b/apps/server/src/orchestration-v2/ThreadSettlementService.ts @@ -237,8 +237,14 @@ export function resolveAutoSettlementAt(input: { return activityAtMs === null ? thread.createdAt : DateTime.makeUnsafe(activityAtMs); } if (input.autoSettleAfterDays === null || activityAtMs === null) return null; - return activityAtMs < input.nowMs - input.autoSettleAfterDays * DAY_MS - ? DateTime.makeUnsafe(activityAtMs) + // A passed wake timer restarts inactivity. An earlier wake can only delay settlement. + const snoozedUntilMs = toMillis(thread.snoozedUntil); + const timerWakeAtMs = + snoozedUntilMs !== null && snoozedUntilMs <= input.nowMs ? snoozedUntilMs : null; + const inactivityAtMs = + latestMillis([activityAtMs, toMillis(thread.lastSnoozeWakeAt), timerWakeAtMs]) ?? activityAtMs; + return inactivityAtMs < input.nowMs - input.autoSettleAfterDays * DAY_MS + ? DateTime.makeUnsafe(inactivityAtMs) : null; } diff --git a/apps/server/src/orchestration-v2/runtimeLayer.test.ts b/apps/server/src/orchestration-v2/runtimeLayer.test.ts index d805bab92d83..b11184f44632 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.test.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts @@ -4548,6 +4548,24 @@ it.layer(layerSharedApplicationDataPlaneTest)("snooze projection", (it) => { assert.deepEqual(thread.snoozedAt, firstSnoozedAt); assert.deepEqual(thread.updatedAt, firstUpdatedAt); + yield* orchestrator.dispatch({ + type: "thread.unsnooze", + commandId: CommandId.make("runtime-layer-snoozed-thread-wake"), + threadId, + reason: "user", + }); + const projections = yield* ProjectionStore.ProjectionStoreV2.pipe( + Effect.provide(ProjectionStore.layer), + ); + const [candidate] = yield* projections.getSettlementCandidates(threadId); + assert.deepEqual(candidate?.lastSnoozeWakeAt, yield* DateTime.now); + yield* orchestrator.dispatch({ + type: "thread.snooze", + commandId: CommandId.make("runtime-layer-snoozed-thread-snooze-after-wake"), + threadId, + snoozedUntil, + }); + yield* orchestrator.dispatch({ type: "message.dispatch", createdBy: "user", diff --git a/packages/contracts/src/orchestrationV2.ts b/packages/contracts/src/orchestrationV2.ts index 7a1e56b6d5e3..591420ce73c6 100644 --- a/packages/contracts/src/orchestrationV2.ts +++ b/packages/contracts/src/orchestrationV2.ts @@ -404,6 +404,8 @@ export const OrchestrationV2AppThread = Schema.Struct({ unsettledAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), snoozedUntil: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), snoozedAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), + /** Manual wakes restart inactivity without changing the sidebar's sort position. */ + lastSnoozeWakeAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), limitRecovery: Schema.optional(Schema.NullOr(OrchestrationV2LimitRecovery)), pinnedAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), autoSettleDisabledAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), @@ -1999,6 +2001,7 @@ export const OrchestrationV2AppThreadJson = OrchestrationV2AppThread.mapFields(( unsettledAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), snoozedUntil: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), snoozedAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), + lastSnoozeWakeAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), pinnedAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), autoSettleDisabledAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), lastVisitedAt: Schema.NullOr(Schema.DateTimeUtcFromString).pipe( From 454b94a13aea918f27bb060d8d054d4cfb791bc2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 12:25:26 -0700 Subject: [PATCH 07/39] feat(desktop): passkeys in the in-app browser on macOS (#16952) Co-authored-by: Ben Davis Co-authored-by: Claude Opus 5.5 (1M context) --- apps/desktop/package.json | 4 +- apps/desktop/src/app/DesktopApp.ts | 3 + apps/desktop/src/main.ts | 2 + apps/desktop/src/preview-pick-preload.ts | 9 + apps/desktop/src/preview/GuestProtocol.ts | 4 + apps/desktop/src/preview/Manager.test.ts | 8 + apps/desktop/src/preview/Manager.ts | 7 + .../desktop/src/preview/PasskeyAttestation.ts | 59 +++ apps/desktop/src/preview/PasskeyBackend.ts | 26 ++ apps/desktop/src/preview/PasskeyBackendMac.ts | 96 +++++ .../desktop/src/preview/PasskeyBridge.test.ts | 246 ++++++++++++ apps/desktop/src/preview/PasskeyBridge.ts | 286 ++++++++++++++ apps/desktop/src/preview/Passkeys.test.ts | 374 ++++++++++++++++++ apps/desktop/src/preview/Passkeys.ts | 242 ++++++++++++ apps/desktop/src/window/DesktopWindow.test.ts | 11 + apps/desktop/src/window/DesktopWindow.ts | 9 + docs/operations/release.md | 10 + pnpm-lock.yaml | 136 +++++++ pnpm-workspace.yaml | 12 + scripts/build-desktop-artifact.test.ts | 91 ++++- scripts/build-desktop-artifact.ts | 107 ++++- scripts/lib/desktop-external-packages.ts | 1 + scripts/package.json | 2 + third-party-licenses.config.json | 21 + 24 files changed, 1762 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src/preview/PasskeyAttestation.ts create mode 100644 apps/desktop/src/preview/PasskeyBackend.ts create mode 100644 apps/desktop/src/preview/PasskeyBackendMac.ts create mode 100644 apps/desktop/src/preview/PasskeyBridge.test.ts create mode 100644 apps/desktop/src/preview/PasskeyBridge.ts create mode 100644 apps/desktop/src/preview/Passkeys.test.ts create mode 100644 apps/desktop/src/preview/Passkeys.ts diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 15bbbda6c6df..551aa17e962e 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -27,9 +27,11 @@ "electron": "44.4.5", "electron-store": "^8.2.0", "electron-updater": "^6.8.9", + "electron-webauthn": "catalog:", "ffi-rs": "1.3.2", "playwright-core": "1.60.0", - "react-grab": "^0.1.32" + "react-grab": "^0.1.32", + "tldts": "catalog:" }, "devDependencies": { "@effect/vitest": "catalog:", diff --git a/apps/desktop/src/app/DesktopApp.ts b/apps/desktop/src/app/DesktopApp.ts index 08318bcd8da5..944077d0e396 100644 --- a/apps/desktop/src/app/DesktopApp.ts +++ b/apps/desktop/src/app/DesktopApp.ts @@ -17,6 +17,7 @@ import * as DesktopAppIdentity from "./DesktopAppIdentity.ts"; import * as DesktopClerk from "./DesktopClerk.ts"; import * as DesktopApplicationMenu from "../window/DesktopApplicationMenu.ts"; import * as DesktopWindow from "../window/DesktopWindow.ts"; +import * as PreviewPasskeys from "../preview/Passkeys.ts"; import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts"; import * as DesktopEnvironment from "./DesktopEnvironment.ts"; import * as DesktopLegacyLocalStorage from "./DesktopLegacyLocalStorage.ts"; @@ -276,6 +277,7 @@ const startup = Effect.gen(function* () { const safeStorage = yield* ElectronSafeStorage.ElectronSafeStorage; const updates = yield* DesktopUpdates.DesktopUpdates; const environment = yield* DesktopEnvironment.DesktopEnvironment; + const previewPasskeys = yield* PreviewPasskeys.PreviewPasskeys; yield* shellEnvironment.installIntoProcess; const hasCommandLinePasswordStore = @@ -329,6 +331,7 @@ const startup = Effect.gen(function* () { }); } yield* appIdentity.configure; + yield* previewPasskeys.configure; yield* applicationMenu.configure; yield* updates.configure; yield* DesktopRemoteUpdates.listen; diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 01c3d7892c66..869961f955b7 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -70,6 +70,7 @@ import * as LinuxBrowserSecret from "./preview/BrowserImport/LinuxBrowserSecret. import * as BrowserSession from "./preview/BrowserSession.ts"; import * as DesktopBrowserHost from "./preview/DesktopBrowserHost.ts"; import * as PreviewManager from "./preview/Manager.ts"; +import * as PreviewPasskeys from "./preview/Passkeys.ts"; import * as DesktopWindow from "./window/DesktopWindow.ts"; import * as DesktopWslBackend from "./wsl/DesktopWslBackend.ts"; import * as DesktopWslEnvironment from "./wsl/DesktopWslEnvironment.ts"; @@ -168,6 +169,7 @@ const layerDesktopPreview = PreviewManager.layer.pipe( // service alongside the manager; both sit on the same BrowserSession. Layer.provideMerge(BrowserImport.layer.pipe(Layer.provide(LinuxBrowserSecret.layer))), Layer.provideMerge(BrowserSession.layer), + Layer.provideMerge(PreviewPasskeys.layer), Layer.provideMerge(layerDesktopFoundation), ); diff --git a/apps/desktop/src/preview-pick-preload.ts b/apps/desktop/src/preview-pick-preload.ts index 84e6abb29ee6..7c4f23e37f50 100644 --- a/apps/desktop/src/preview-pick-preload.ts +++ b/apps/desktop/src/preview-pick-preload.ts @@ -1 +1,10 @@ +import { ipcRenderer } from "electron"; + +import { PASSKEY_BRIDGE_ARGUMENT } from "./preview/GuestProtocol.ts"; +import { installPasskeyBridge } from "./preview/PasskeyBridge.ts"; import "./preview/PickPreload.ts"; + +// Electron hands webPreferences.additionalArguments to sandboxed preloads in process.argv. +if (process.argv.includes(PASSKEY_BRIDGE_ARGUMENT)) { + installPasskeyBridge((channel, publicKey) => ipcRenderer.invoke(channel, publicKey)); +} diff --git a/apps/desktop/src/preview/GuestProtocol.ts b/apps/desktop/src/preview/GuestProtocol.ts index 5019076a100d..4a3eea297c12 100644 --- a/apps/desktop/src/preview/GuestProtocol.ts +++ b/apps/desktop/src/preview/GuestProtocol.ts @@ -11,3 +11,7 @@ export const RECORDING_POINTER_CHANNEL = "preview:recording-pointer"; export const RECORDING_KEY_CHANNEL = "preview:recording-key"; export const RECORDING_INPUT_CHANNEL = "preview:recording-input"; export const RECORDING_CONTROLLER_CHANNEL = "preview:recording-controller"; +export const PASSKEY_CREATE_CHANNEL = "preview:passkey-create"; +export const PASSKEY_GET_CHANNEL = "preview:passkey-get"; +/** Renderer argument that turns on the guest passkey bridge; see Passkeys.ts. */ +export const PASSKEY_BRIDGE_ARGUMENT = "--t3code-preview-passkey-bridge"; diff --git a/apps/desktop/src/preview/Manager.test.ts b/apps/desktop/src/preview/Manager.test.ts index 84c6273aad62..c13e2ce2868d 100644 --- a/apps/desktop/src/preview/Manager.test.ts +++ b/apps/desktop/src/preview/Manager.test.ts @@ -29,6 +29,7 @@ import * as ElectronWindow from "../electron/ElectronWindow.ts"; import * as BrowserSession from "./BrowserSession.ts"; import * as DesktopBrowserHost from "./DesktopBrowserHost.ts"; import * as PreviewManager from "./Manager.ts"; +import * as PreviewPasskeys from "./Passkeys.ts"; describe("fitPictureInPictureContentSize", () => { it("preserves the PiP content area across aspect-ratio changes", () => { @@ -301,6 +302,13 @@ const managerLayer = (platform: NodeJS.Platform = "darwin") => ), Layer.provideMerge(DesktopBrowserHost.layer), Layer.provideMerge(layerBrowserSession), + Layer.provideMerge( + Layer.mock(PreviewPasskeys.PreviewPasskeys)({ + bridgeEnabled: false, + installSessionHandlers: () => {}, + attachGuest: () => () => {}, + }), + ), Layer.provideMerge(layerEnvironment), Layer.provideMerge(layerFileSystem), Layer.provideMerge(Path.layer), diff --git a/apps/desktop/src/preview/Manager.ts b/apps/desktop/src/preview/Manager.ts index 123943f70f9a..58a7413ede2c 100644 --- a/apps/desktop/src/preview/Manager.ts +++ b/apps/desktop/src/preview/Manager.ts @@ -64,6 +64,7 @@ import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; import * as DesktopRendererHistory from "../telemetry/DesktopRendererHistory.ts"; import { MENU_ACTION_CHANNEL, PREVIEW_PICTURE_IN_PICTURE_FRAME_CHANNEL } from "../ipc/channels.ts"; import * as DesktopBrowserHost from "./DesktopBrowserHost.ts"; +import * as PreviewPasskeys from "./Passkeys.ts"; import * as BrowserSession from "./BrowserSession.ts"; import { ANNOTATION_CAPTURED_CHANNEL, @@ -548,6 +549,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function const crypto = yield* Crypto.Crypto; const parentScope = yield* Scope.Scope; const browserHost = yield* DesktopBrowserHost.DesktopBrowserHost; + const passkeys = yield* PreviewPasskeys.PreviewPasskeys; const context = yield* Effect.context(); const runFork = Effect.runForkWith(context); const resolvedArtifactDirectory = path.resolve(artifactDirectory); @@ -1254,6 +1256,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function ) { const scope = yield* Scope.fork(parentScope, "sequential"); const attachmentId = Symbol(); + let detachPasskeys = () => {}; let documentId = 0; let nextRequestId = 0; let activeCapture: { @@ -1589,6 +1592,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function wc.ipc.off(HUMAN_INPUT_CHANNEL, humanInput); wc.ipc.off(RECORDING_INPUT_CHANNEL, recordingInput); wc.ipc.off(MOUSE_NAVIGATE_CHANNEL, mouseNavigate); + detachPasskeys(); }).pipe(Effect.ignore), ); const install = Effect.fn("PreviewManager.installWebContentsListeners")(function* () { @@ -1609,6 +1613,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function wc.ipc.on(HUMAN_INPUT_CHANNEL, humanInput); wc.ipc.on(RECORDING_INPUT_CHANNEL, recordingInput); wc.ipc.on(MOUSE_NAVIGATE_CHANNEL, mouseNavigate); + detachPasskeys = passkeys.attachGuest(wc); wc.setWindowOpenHandler((details) => { const action = previewWindowOpenAction(details); if (action === "popup") { @@ -3686,6 +3691,7 @@ export const make = Effect.gen(function* PreviewManagerMake() { const environment = yield* DesktopEnvironment.DesktopEnvironment; const browserSession = yield* BrowserSession.BrowserSession; const browserHost = yield* DesktopBrowserHost.DesktopBrowserHost; + const passkeys = yield* PreviewPasskeys.PreviewPasskeys; const downloadSessions = new WeakSet(); const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -3750,6 +3756,7 @@ export const make = Effect.gen(function* PreviewManagerMake() { ), ); placeServerDownloads(session); + passkeys.installSessionHandlers(session); return session; }, ), diff --git a/apps/desktop/src/preview/PasskeyAttestation.ts b/apps/desktop/src/preview/PasskeyAttestation.ts new file mode 100644 index 000000000000..17927a2957da --- /dev/null +++ b/apps/desktop/src/preview/PasskeyAttestation.ts @@ -0,0 +1,59 @@ +/** + * Reads the raw `authData` bytes out of a CBOR attestation object + * (`{ fmt, attStmt, authData }`). Returns undefined for anything malformed. + * Only the definite-length encodings authenticators emit are understood. + */ +export const authenticatorDataFromAttestation = (bytes: Uint8Array) => { + let offset = 0; + const readHead = () => { + const initial = bytes[offset++]; + if (initial === undefined) return undefined; + const info = initial & 31; + let length = info; + if (info >= 24) { + if (info > 27) return undefined; + const size = 1 << (info - 24); + if (offset + size > bytes.length) return undefined; + length = 0; + for (let index = 0; index < size; index++) length = length * 256 + (bytes[offset++] ?? 0); + } + return { major: initial >> 5, length }; + }; + const skipValue = (): boolean => { + const head = readHead(); + if (!head) return false; + switch (head.major) { + case 2: + case 3: + offset += head.length; + return offset <= bytes.length; + case 4: + for (let index = 0; index < head.length; index++) if (!skipValue()) return false; + return true; + case 5: + for (let index = 0; index < head.length * 2; index++) if (!skipValue()) return false; + return true; + case 6: + return skipValue(); + default: + return true; + } + }; + + const map = readHead(); + if (map?.major !== 5) return undefined; + for (let entry = 0; entry < map.length; entry++) { + const key = readHead(); + if (key?.major !== 3 || offset + key.length > bytes.length) return undefined; + const name = new TextDecoder().decode(bytes.subarray(offset, offset + key.length)); + offset += key.length; + if (name !== "authData") { + if (!skipValue()) return undefined; + continue; + } + const value = readHead(); + if (value?.major !== 2 || offset + value.length > bytes.length) return undefined; + return bytes.slice(offset, offset + value.length); + } + return undefined; +}; diff --git a/apps/desktop/src/preview/PasskeyBackend.ts b/apps/desktop/src/preview/PasskeyBackend.ts new file mode 100644 index 000000000000..ac174d2759a4 --- /dev/null +++ b/apps/desktop/src/preview/PasskeyBackend.ts @@ -0,0 +1,26 @@ +import type { PasskeyCeremonyResult } from "./PasskeyBridge.ts"; + +/** What the shared bridge has already checked before a backend runs a ceremony. */ +export interface PasskeyCeremonyContext { + /** The committed origin of the frame that asked; never the page's own claim. */ + readonly origin: string; + /** The window the system sheet attaches to. */ + readonly nativeWindowHandle: Buffer; +} + +/** + * One platform's way to create and use passkeys for a preview page. The + * bridge in `Passkeys.ts` owns everything platform-neutral: which frame may + * ask, focus, one ceremony at a time, the deadline, and dropping results for + * a page that navigated away. A backend only talks to the platform. + */ +export interface PasskeyBackend { + readonly create: ( + publicKey: PublicKeyCredentialCreationOptions, + context: PasskeyCeremonyContext, + ) => Promise; + readonly get: ( + publicKey: PublicKeyCredentialRequestOptions, + context: PasskeyCeremonyContext, + ) => Promise; +} diff --git a/apps/desktop/src/preview/PasskeyBackendMac.ts b/apps/desktop/src/preview/PasskeyBackendMac.ts new file mode 100644 index 000000000000..a21d94df1cbe --- /dev/null +++ b/apps/desktop/src/preview/PasskeyBackendMac.ts @@ -0,0 +1,96 @@ +import { getPublicSuffix } from "tldts"; + +import type { PasskeyBackend, PasskeyCeremonyContext } from "./PasskeyBackend.ts"; +import { authenticatorDataFromAttestation } from "./PasskeyAttestation.ts"; +import type { PasskeyCeremonyResult } from "./PasskeyBridge.ts"; + +const ES256 = -7; + +// WebAuthn rejects RP IDs that are public suffixes, private registries +// included, so one github.io site cannot mint passkeys for every other one. +const isPublicSuffix = (domain: string) => + domain !== "localhost" && getPublicSuffix(domain, { allowPrivateDomains: true }) === domain; + +// Loaded only once a ceremony runs: the module and its native addon ship on macOS alone. +const nativeOptions = async (context: PasskeyCeremonyContext) => ({ + webauthn: await import("electron-webauthn"), + options: { + currentOrigin: context.origin, + topFrameOrigin: context.origin, + nativeWindowHandle: context.nativeWindowHandle, + isPublicSuffix, + }, +}); + +const isBufferSource = (value: unknown): value is BufferSource => + value instanceof ArrayBuffer || ArrayBuffer.isView(value); + +const toBase64Url = (value: BufferSource) => + Buffer.from( + ArrayBuffer.isView(value) + ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) + : new Uint8Array(value), + ).toString("base64url"); + +/** + * macOS passkeys through AuthenticationServices (`electron-webauthn`): the + * system sheet offers iCloud Keychain, password managers, phones, and security + * keys. Needs Apple's managed browser entitlement in the signed build. + */ +export const macPasskeyBackend: PasskeyBackend = { + create: async (publicKey, context): Promise => { + // The native layer only converts P-256 keys and never settles for any + // other algorithm, so ES256 is the only one it may negotiate. + const params: unknown = publicKey.pubKeyCredParams; + if (params !== undefined && !Array.isArray(params)) + return { success: false, error: "TypeError" }; + const allowsEs256 = + params === undefined || + params.length === 0 || + params.some( + (param: unknown) => + typeof param === "object" && param !== null && "alg" in param && param.alg === ES256, + ); + if (!allowsEs256) return { success: false, error: "NotSupportedError" }; + const { webauthn, options } = await nativeOptions(context); + const result = await webauthn.createCredential( + { ...publicKey, pubKeyCredParams: [{ type: "public-key", alg: ES256 }] }, + options, + ); + if (!result.success) return { success: false, error: result.error }; + // The native layer reports parsed authenticator data as JSON and claims + // every credential is a synced platform passkey; neither is reliable. + const authData = authenticatorDataFromAttestation( + Buffer.from(result.data.attestationObject, "base64url"), + ); + // Sites verify the new credential from its authenticator data; without it + // the passkey is unusable, so say so now rather than let sign-up fail later. + if (!authData) return { success: false, error: "NotAllowedError" }; + return { + success: true, + data: { + ...result.data, + authData: Buffer.from(authData).toString("base64url"), + transports: [], + }, + }; + }, + get: async (publicKey, context): Promise => { + const { webauthn, options } = await nativeOptions(context); + // WebAuthn defaults the RP ID to the caller's host; the native layer requires it. + const result = await webauthn.getCredential( + { ...publicKey, rpId: publicKey.rpId ?? new URL(context.origin).hostname }, + options, + ); + if (!result.success) return { success: false, error: result.error }; + // The native layer applies the site's allow list to platform passkeys only, + // so a security key can answer with a credential the site did not ask for. + const allowed = (Array.isArray(publicKey.allowCredentials) ? publicKey.allowCredentials : []) + .filter((credential) => credential?.type === "public-key" && isBufferSource(credential.id)) + .map((credential) => toBase64Url(credential.id)); + if (allowed.length > 0 && !allowed.includes(result.data.credentialId)) { + return { success: false, error: "NotAllowedError" }; + } + return result; + }, +}; diff --git a/apps/desktop/src/preview/PasskeyBridge.test.ts b/apps/desktop/src/preview/PasskeyBridge.test.ts new file mode 100644 index 000000000000..3ca93190ae51 --- /dev/null +++ b/apps/desktop/src/preview/PasskeyBridge.test.ts @@ -0,0 +1,246 @@ +import { assert, describe, it } from "@effect/vitest"; +import { afterEach, vi } from "vite-plus/test"; + +import { PASSKEY_CREATE_CHANNEL, PASSKEY_GET_CHANNEL } from "./GuestProtocol.ts"; +import { installPasskeyBridge } from "./PasskeyBridge.ts"; + +const base64Url = (bytes: ReadonlyArray) => Buffer.from(bytes).toString("base64url"); +const bytesOf = (buffer: ArrayBuffer | null) => (buffer ? [...new Uint8Array(buffer)] : null); + +const assertionData = { + credentialId: base64Url([1, 2, 3]), + clientDataJSON: base64Url([4]), + authenticatorData: base64Url([5]), + signature: base64Url([6, 7]), + userHandle: base64Url([8]), + extensions: { prf: { results: { first: base64Url([9]) } } }, +}; + +const attestationData = { + credentialId: base64Url([1, 2, 3]), + clientDataJSON: base64Url([4]), + attestationObject: base64Url([10]), + authData: base64Url([11]), + publicKey: base64Url([12]), + publicKeyAlgorithm: -7, + transports: [], + extensions: { credProps: { rk: true } }, +}; + +const creationOptions: PublicKeyCredentialCreationOptions = { + challenge: new Uint8Array([1]), + rp: { name: "Example" }, + user: { id: new Uint8Array([2]), name: "alice", displayName: "Alice" }, + pubKeyCredParams: [{ type: "public-key", alg: -7 }], +}; + +const illegalInvocation = (): never => { + throw new TypeError("Illegal invocation"); +}; + +/** + * Installs the bridge over fresh stand-ins for the page's DOM classes. Like + * Chromium's, their getters only work on objects the browser created itself. + */ +const install = (respond: (channel: string) => unknown) => { + const nativeCreate = vi.fn(async () => "native-create"); + const nativeGet = vi.fn(async () => "native-get"); + class FakeCredentialsContainer { + create() { + return nativeCreate(); + } + get() { + return nativeGet(); + } + } + class FakePublicKeyCredential { + get id() { + return illegalInvocation(); + } + get response() { + return illegalInvocation(); + } + } + class FakeAttestationResponse { + get clientDataJSON() { + return illegalInvocation(); + } + } + class FakeAssertionResponse { + get signature() { + return illegalInvocation(); + } + } + vi.stubGlobal("CredentialsContainer", FakeCredentialsContainer); + vi.stubGlobal("PublicKeyCredential", FakePublicKeyCredential); + vi.stubGlobal("AuthenticatorAttestationResponse", FakeAttestationResponse); + vi.stubGlobal("AuthenticatorAssertionResponse", FakeAssertionResponse); + + const invoke = vi.fn(async (channel: string, _publicKey: unknown) => respond(channel)); + installPasskeyBridge(invoke); + return { + invoke, + nativeCreate, + nativeGet, + credentials: new FakeCredentialsContainer() as unknown as CredentialsContainer, + FakePublicKeyCredential, + FakeAttestationResponse, + FakeAssertionResponse, + }; +}; + +describe("installPasskeyBridge", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("serves passkey sign-in through the main process with a credential pages can verify", async () => { + const { invoke, credentials, FakePublicKeyCredential, FakeAssertionResponse } = install(() => ({ + success: true, + data: assertionData, + })); + const challenge = new Uint8Array([0, 1, 2, 3]).subarray(1); + + const credential = (await credentials.get({ + publicKey: { + challenge, + rpId: "example.com", + allowCredentials: [{ type: "public-key", id: new Uint8Array([1, 2, 3]) }], + // Unknown members the page adds must not break IPC cloning. + extensions: { onHint: () => {} } as AuthenticationExtensionsClientInputs, + }, + })) as PublicKeyCredential; + + assert.deepStrictEqual(invoke.mock.calls[0], [ + PASSKEY_GET_CHANNEL, + { + challenge: new Uint8Array([1, 2, 3]), + rpId: "example.com", + allowCredentials: [{ type: "public-key", id: new Uint8Array([1, 2, 3]) }], + extensions: {}, + }, + ]); + const response = credential.response as AuthenticatorAssertionResponse; + assert.instanceOf(credential, FakePublicKeyCredential); + assert.instanceOf(response, FakeAssertionResponse); + assert.strictEqual(credential.id, assertionData.credentialId); + assert.deepStrictEqual(bytesOf(credential.rawId), [1, 2, 3]); + assert.deepStrictEqual(bytesOf(response.signature), [6, 7]); + assert.deepStrictEqual(bytesOf(response.userHandle), [8]); + assert.deepStrictEqual( + bytesOf(credential.getClientExtensionResults().prf?.results?.first as ArrayBuffer), + [9], + ); + assert.deepStrictEqual(credential.toJSON(), { + id: assertionData.credentialId, + rawId: assertionData.credentialId, + type: "public-key", + response: { + clientDataJSON: assertionData.clientDataJSON, + authenticatorData: assertionData.authenticatorData, + signature: assertionData.signature, + userHandle: assertionData.userHandle, + }, + clientExtensionResults: { prf: { results: { first: base64Url([9]) } } }, + }); + }); + + it("returns registrations with their attestation and public key", async () => { + const { invoke, credentials, FakeAttestationResponse } = install(() => ({ + success: true, + data: attestationData, + })); + + const credential = (await credentials.create({ + publicKey: creationOptions, + })) as PublicKeyCredential; + + assert.strictEqual(invoke.mock.calls[0]?.[0], PASSKEY_CREATE_CHANNEL); + const response = credential.response as AuthenticatorAttestationResponse; + assert.instanceOf(response, FakeAttestationResponse); + assert.isNull(credential.authenticatorAttachment); + assert.deepStrictEqual(bytesOf(response.attestationObject), [10]); + assert.deepStrictEqual(bytesOf(response.getAuthenticatorData()), [11]); + assert.deepStrictEqual(bytesOf(response.getPublicKey()), [12]); + assert.strictEqual(response.getPublicKeyAlgorithm(), -7); + assert.deepStrictEqual(credential.getClientExtensionResults(), { credProps: { rk: true } }); + assert.deepStrictEqual(credential.toJSON(), { + id: attestationData.credentialId, + rawId: attestationData.credentialId, + type: "public-key", + response: { + clientDataJSON: attestationData.clientDataJSON, + attestationObject: attestationData.attestationObject, + authenticatorData: attestationData.authData, + transports: [], + publicKey: attestationData.publicKey, + publicKeyAlgorithm: -7, + }, + clientExtensionResults: { credProps: { rk: true } }, + }); + }); + + it("leaves autofill and non-passkey requests to Chromium", async () => { + const { invoke, credentials, nativeCreate, nativeGet } = install(() => ({ + success: true, + data: assertionData, + })); + + await credentials.get({ + mediation: "conditional", + publicKey: { challenge: new Uint8Array([1]) }, + }); + await credentials.get({ password: true } as CredentialRequestOptions); + // Conditional create upgrades a password sign-in silently; no modal sheet. + const upgrade = { mediation: "conditional", publicKey: creationOptions } as const; + await credentials.create(upgrade); + await credentials.create({ password: {} } as CredentialCreationOptions); + assert.strictEqual(invoke.mock.calls.length, 0); + assert.strictEqual(nativeGet.mock.calls.length, 2); + assert.strictEqual(nativeCreate.mock.calls.length, 2); + assert.isFalse(await PublicKeyCredential.isConditionalMediationAvailable()); + assert.isTrue(await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()); + }); + + it("rejects with the WebAuthn error the ceremony ended with", async () => { + const { credentials } = install((channel) => ({ + success: false, + error: channel === PASSKEY_CREATE_CHANNEL ? "InvalidStateError" : "TypeError", + })); + const created = await credentials + .create({ publicKey: creationOptions }) + .catch((error: unknown) => error); + assert.instanceOf(created, DOMException); + assert.strictEqual((created as DOMException).name, "InvalidStateError"); + const fetched = await credentials + .get({ publicKey: { challenge: new Uint8Array([1]) } }) + .catch((error: unknown) => error); + assert.instanceOf(fetched, TypeError); + }); + + it("refuses when the main process has no handler for the page", async () => { + const { credentials } = install(() => { + throw new Error("Error invoking remote method 'preview:passkey-get': No handler registered"); + }); + + const error = await credentials + .get({ publicKey: { challenge: new Uint8Array([1]) } }) + .catch((caught: unknown) => caught); + assert.instanceOf(error, DOMException); + assert.strictEqual((error as DOMException).name, "NotAllowedError"); + }); + + it("rejects as soon as the page aborts, without waiting on the system sheet", async () => { + const { credentials } = install(() => new Promise(() => {})); + const controller = new AbortController(); + + const pending = credentials + .get({ publicKey: { challenge: new Uint8Array([1]) }, signal: controller.signal }) + .catch((error: unknown) => error); + controller.abort(); + + const error = await pending; + assert.instanceOf(error, DOMException); + assert.strictEqual((error as DOMException).name, "AbortError"); + }); +}); diff --git a/apps/desktop/src/preview/PasskeyBridge.ts b/apps/desktop/src/preview/PasskeyBridge.ts new file mode 100644 index 000000000000..cea175c29633 --- /dev/null +++ b/apps/desktop/src/preview/PasskeyBridge.ts @@ -0,0 +1,286 @@ +import type { CreateCredentialResult, GetCredentialResult } from "electron-webauthn"; + +import { PASSKEY_CREATE_CHANNEL, PASSKEY_GET_CHANNEL } from "./GuestProtocol.ts"; + +/** What the main process answers a guest passkey ceremony with. */ +export type PasskeyCeremonyResult = + | Extract + | Extract + | { readonly success: false; readonly error: string }; + +type PasskeyChannel = typeof PASSKEY_CREATE_CHANNEL | typeof PASSKEY_GET_CHANNEL; +// Credential Management Level 1 added `mediation` to creation requests. +type CreationOptions = CredentialCreationOptions & { + readonly mediation?: CredentialMediationRequirement; +}; +type InvokePasskeyCeremony = (channel: PasskeyChannel, publicKey: unknown) => Promise; + +const NOT_ALLOWED_MESSAGE = + "The operation either timed out or was not allowed. See: https://www.w3.org/TR/webauthn-2/#sctn-privacy-considerations-client."; +const DOM_EXCEPTION_NAMES = new Set([ + "AbortError", + "InvalidStateError", + "NotAllowedError", + "NotSupportedError", + "SecurityError", +]); + +const fromBase64Url = (value: string) => { + const base64 = value.replaceAll("-", "+").replaceAll("_", "/"); + const binary = atob(base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=")); + return Uint8Array.from(binary, (character) => character.charCodeAt(0)).buffer; +}; + +const toBase64Url = (buffer: ArrayBuffer) => + btoa(String.fromCharCode(...new Uint8Array(buffer))) + .replaceAll("+", "-") + .replaceAll("/", "_") + .replace(/=+$/u, ""); + +const ceremonyError = (name: string) => + name === "TypeError" + ? new TypeError("The passkey request options are invalid.") + : new DOMException( + NOT_ALLOWED_MESSAGE, + DOM_EXCEPTION_NAMES.has(name) ? name : "NotAllowedError", + ); + +/** + * Copies request options into plain IPC-cloneable data. Chromium ignores + * members it does not know, so functions and other uncloneable values drop out + * here instead of failing the whole ceremony. + */ +const toCloneable = (value: unknown): unknown => { + if (value instanceof ArrayBuffer) return value.slice(0); + if (ArrayBuffer.isView(value)) { + return new Uint8Array(value.buffer, value.byteOffset, value.byteLength).slice(); + } + if (Array.isArray(value)) return value.map(toCloneable); + if (typeof value === "object" && value !== null) { + return Object.fromEntries( + Object.entries(value).flatMap(([key, member]) => + typeof member === "function" || member === undefined ? [] : [[key, toCloneable(member)]], + ), + ); + } + return value; +}; + +// The objects below borrow the native prototypes so `instanceof` checks pass. +// Their own properties shadow the native getters, which only work on objects +// Chromium created itself. +const withValues = (target: T, values: Record) => { + for (const [key, value] of Object.entries(values)) { + Object.defineProperty(target, key, { value, enumerable: true, configurable: true }); + } + return target; +}; + +const prfOutputs = (prf: { + readonly enabled?: boolean; + readonly results?: { readonly first?: string; readonly second?: string }; +}): AuthenticationExtensionsPRFOutputs => { + const first = prf.results?.first; + const second = prf.results?.second; + return { + ...(prf.enabled === undefined ? {} : { enabled: prf.enabled }), + ...(first === undefined + ? {} + : { + results: { + first: fromBase64Url(first), + ...(second === undefined ? {} : { second: fromBase64Url(second) }), + }, + }), + }; +}; + +const extensionResultsJson = (results: AuthenticationExtensionsClientOutputs) => + JSON.parse( + JSON.stringify(results, (_key, value: unknown) => + value instanceof ArrayBuffer ? toBase64Url(value) : value, + ), + ) as unknown; + +const makeCredential = (input: { + readonly id: string; + readonly authenticatorAttachment: AuthenticatorAttachment | null; + readonly response: AuthenticatorResponse; + readonly responseJson: Record; + readonly extensionResults: AuthenticationExtensionsClientOutputs; +}) => + withValues(Object.create(PublicKeyCredential.prototype) as PublicKeyCredential, { + id: input.id, + rawId: fromBase64Url(input.id), + type: "public-key", + authenticatorAttachment: input.authenticatorAttachment, + response: input.response, + getClientExtensionResults: () => input.extensionResults, + toJSON: () => ({ + id: input.id, + rawId: input.id, + type: "public-key", + ...(input.authenticatorAttachment === null + ? {} + : { authenticatorAttachment: input.authenticatorAttachment }), + response: input.responseJson, + clientExtensionResults: extensionResultsJson(input.extensionResults), + }), + }); + +const credentialFromCreateResult = ( + data: Extract["data"], +) => { + const transports = [...data.transports]; + const publicKey = data.publicKey.length > 0 ? data.publicKey : null; + const extensionResults: AuthenticationExtensionsClientOutputs = { + ...(data.extensions.credProps ? { credProps: { ...data.extensions.credProps } } : {}), + ...(data.extensions.prf ? { prf: prfOutputs(data.extensions.prf) } : {}), + ...(data.extensions.largeBlob ? { largeBlob: { ...data.extensions.largeBlob } } : {}), + }; + return makeCredential({ + id: data.credentialId, + // The native layer cannot tell a synced passkey from a security key. + authenticatorAttachment: null, + response: withValues( + Object.create(AuthenticatorAttestationResponse.prototype) as AuthenticatorAttestationResponse, + { + clientDataJSON: fromBase64Url(data.clientDataJSON), + attestationObject: fromBase64Url(data.attestationObject), + getTransports: () => [...transports], + getAuthenticatorData: () => fromBase64Url(data.authData), + getPublicKey: () => (publicKey === null ? null : fromBase64Url(publicKey)), + getPublicKeyAlgorithm: () => data.publicKeyAlgorithm, + }, + ), + responseJson: { + clientDataJSON: data.clientDataJSON, + attestationObject: data.attestationObject, + authenticatorData: data.authData, + transports, + ...(publicKey === null ? {} : { publicKey }), + publicKeyAlgorithm: data.publicKeyAlgorithm, + }, + extensionResults, + }); +}; + +const credentialFromGetResult = (data: Extract["data"]) => { + const userHandle = data.userHandle.length > 0 ? data.userHandle : null; + const { prf, largeBlob } = data.extensions ?? {}; + const extensionResults: AuthenticationExtensionsClientOutputs = { + ...(prf ? { prf: prfOutputs(prf) } : {}), + ...(largeBlob + ? { + largeBlob: { + ...(largeBlob.blob === undefined ? {} : { blob: fromBase64Url(largeBlob.blob) }), + ...(largeBlob.written === undefined ? {} : { written: largeBlob.written }), + }, + } + : {}), + }; + return makeCredential({ + id: data.credentialId, + authenticatorAttachment: null, + response: withValues( + Object.create(AuthenticatorAssertionResponse.prototype) as AuthenticatorAssertionResponse, + { + clientDataJSON: fromBase64Url(data.clientDataJSON), + authenticatorData: fromBase64Url(data.authenticatorData), + signature: fromBase64Url(data.signature), + userHandle: userHandle === null ? null : fromBase64Url(userHandle), + }, + ), + responseJson: { + clientDataJSON: data.clientDataJSON, + authenticatorData: data.authenticatorData, + signature: data.signature, + ...(userHandle === null ? {} : { userHandle }), + }, + extensionResults, + }); +}; + +const abortReason = (signal: AbortSignal) => + signal.reason ?? new DOMException("The operation was aborted.", "AbortError"); + +/** + * Routes this page's WebAuthn ceremonies to the system passkey sheet through + * the main process, which supplies the frame's real origin. Runs in the page's + * own world (the preview preload has contextIsolation off), before any page + * script. Conditional (autofill and automatic upgrade) requests stay native: + * the system sheet is modal and must not open on its own. + */ +export function installPasskeyBridge(invoke: InvokePasskeyCeremony) { + // Insecure contexts have no WebAuthn to bridge. + if (typeof CredentialsContainer === "undefined" || typeof PublicKeyCredential === "undefined") { + return; + } + const run = async ( + channel: PasskeyChannel, + publicKey: unknown, + signal: AbortSignal | undefined, + ): Promise => { + if (signal?.aborted) throw abortReason(signal); + const ceremony = invoke(channel, toCloneable(publicKey)).then( + (value) => { + const result = value as PasskeyCeremonyResult; + if (!result.success) throw ceremonyError(result.error); + return "attestationObject" in result.data + ? credentialFromCreateResult(result.data) + : credentialFromGetResult(result.data); + }, + // No handler yet (or any more) for this guest: answer like a refusal. + (error: unknown) => { + throw error instanceof DOMException || error instanceof TypeError + ? error + : ceremonyError("NotAllowedError"); + }, + ); + if (!signal) return ceremony; + return new Promise((resolve, reject) => { + const onAbort = () => reject(abortReason(signal)); + signal.addEventListener("abort", onAbort, { once: true }); + ceremony.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort)); + }); + }; + + const container = CredentialsContainer.prototype; + const nativeCreate = container.create; + const nativeGet = container.get; + const define = (target: object, key: string, value: unknown) => + Object.defineProperty(target, key, { value, configurable: true, writable: true }); + + define( + container, + "create", + function create(this: CredentialsContainer, options?: CreationOptions) { + return options?.publicKey && options.mediation !== "conditional" + ? run(PASSKEY_CREATE_CHANNEL, options.publicKey, options.signal) + : Reflect.apply(nativeCreate, this, [options]); + }, + ); + define( + container, + "get", + function get(this: CredentialsContainer, options?: CredentialRequestOptions) { + return options?.publicKey && options.mediation !== "conditional" + ? run(PASSKEY_GET_CHANNEL, options.publicKey, options.signal) + : Reflect.apply(nativeGet, this, [options]); + }, + ); + + define(PublicKeyCredential, "isUserVerifyingPlatformAuthenticatorAvailable", async () => true); + define(PublicKeyCredential, "isConditionalMediationAvailable", async () => false); + const nativeCapabilities = PublicKeyCredential.getClientCapabilities; + if (typeof nativeCapabilities === "function") { + define(PublicKeyCredential, "getClientCapabilities", async () => ({ + ...(await Reflect.apply(nativeCapabilities, PublicKeyCredential, [])), + conditionalCreate: false, + conditionalGet: false, + hybridTransport: true, + passkeyPlatformAuthenticator: true, + userVerifyingPlatformAuthenticator: true, + })); + } +} diff --git a/apps/desktop/src/preview/Passkeys.test.ts b/apps/desktop/src/preview/Passkeys.test.ts new file mode 100644 index 000000000000..0fe4ed8be682 --- /dev/null +++ b/apps/desktop/src/preview/Passkeys.test.ts @@ -0,0 +1,374 @@ +import { assert, describe, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Fiber from "effect/Fiber"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import { afterEach, beforeEach, vi } from "vite-plus/test"; + +const electron = vi.hoisted(() => ({ + configureWebAuthn: vi.fn(), + showMessageBox: vi.fn(), + fromWebContents: vi.fn(), +})); +const webauthn = vi.hoisted(() => ({ createCredential: vi.fn(), getCredential: vi.fn() })); + +vi.mock("electron", () => ({ + app: { configureWebAuthn: electron.configureWebAuthn }, + dialog: { showMessageBox: electron.showMessageBox }, + BrowserWindow: { fromWebContents: electron.fromWebContents }, + webContents: { fromFrame: () => undefined }, +})); +vi.mock("electron-webauthn", () => webauthn); + +import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; +import { PASSKEY_CREATE_CHANNEL, PASSKEY_GET_CHANNEL } from "./GuestProtocol.ts"; +import * as PreviewPasskeys from "./Passkeys.ts"; + +const layerFor = (packageJson: object, isPackaged = true) => + PreviewPasskeys.layer.pipe( + Layer.provide( + Layer.succeed( + DesktopEnvironment.DesktopEnvironment, + DesktopEnvironment.DesktopEnvironment.of({ + platform: "darwin", + isPackaged, + appRoot: "/app", + path: { join: (...parts: ReadonlyArray) => parts.join("/") }, + } as DesktopEnvironment.DesktopEnvironment["Service"]), + ), + ), + Layer.provide( + FileSystem.layerNoop({ + readFileString: () => Effect.succeed(JSON.stringify(packageJson)), + }), + ), + ); + +type Handler = (event: { readonly senderFrame: object | null }, publicKey: unknown) => unknown; + +const makeGuest = (origin = "https://accounts.example.com") => { + const handlers = new Map(); + const state = { focused: true }; + const mainFrame = { origin, isDestroyed: () => false }; + const guest = { + mainFrame, + hostWebContents: {}, + isFocused: () => state.focused, + ipc: { + handle: (channel: string, handler: Handler) => handlers.set(channel, handler), + removeHandler: (channel: string) => handlers.delete(channel), + }, + }; + const call = (channel: string, publicKey: unknown, senderFrame: object | null = mainFrame) => + Effect.promise(async () => handlers.get(channel)?.({ senderFrame }, publicKey)); + return { guest: guest as unknown as Electron.WebContents, mainFrame, handlers, state, call }; +}; + +const bridgeLayer = layerFor({ t3codeWebAuthn: { browserPasskeys: true } }); + +/** CBOR `{ fmt: "packed", attStmt: { alg: -7, sig }, authData }`, as authenticators encode it. */ +const attestationObject = (authData: ReadonlyArray) => { + const text = (value: string) => [0x60 + value.length, ...Buffer.from(value)]; + const bytes = (value: ReadonlyArray) => [0x58, value.length, ...value]; + return Buffer.from([ + 0xa3, + ...text("fmt"), + ...text("packed"), + ...text("attStmt"), + 0xa2, + ...text("alg"), + 0x26, + ...text("sig"), + ...bytes(Array.from({ length: 70 }, () => 7)), + ...text("authData"), + ...bytes(authData), + ]).toString("base64url"); +}; + +describe("PreviewPasskeys", () => { + beforeEach(() => { + vi.clearAllMocks(); + electron.fromWebContents.mockReturnValue({ + isDestroyed: () => false, + getNativeWindowHandle: () => Buffer.from([1]), + }); + }); + // Restored here so a failing fake-timer test cannot leak fake timers into the next one. + afterEach(() => { + vi.useRealTimers(); + }); + + it.effect("pins each guest ceremony to the origin of the frame that asked", () => + Effect.gen(function* () { + const passkeys = yield* PreviewPasskeys.PreviewPasskeys; + const { guest, handlers, call } = makeGuest(); + webauthn.getCredential.mockResolvedValue({ + success: false, + error: "NotAllowedError", + errorObject: new Error("native detail"), + }); + + const detach = passkeys.attachGuest(guest); + const publicKey = { challenge: new Uint8Array([1]), rpId: "example.com" }; + const result = yield* call(PASSKEY_GET_CHANNEL, publicKey); + + assert.deepStrictEqual(result, { success: false, error: "NotAllowedError" }); + const [sentPublicKey, options] = webauthn.getCredential.mock.calls[0] ?? []; + assert.deepStrictEqual(sentPublicKey, publicKey); + assert.strictEqual(options.currentOrigin, "https://accounts.example.com"); + assert.strictEqual(options.topFrameOrigin, "https://accounts.example.com"); + assert.isTrue(options.isPublicSuffix("com")); + assert.isTrue(options.isPublicSuffix("github.io")); + assert.isFalse(options.isPublicSuffix("example.com")); + assert.isFalse(options.isPublicSuffix("localhost")); + + // WebAuthn defaults the RP ID to the caller's own host. + yield* call(PASSKEY_GET_CHANNEL, { challenge: new Uint8Array([1]) }); + assert.strictEqual(webauthn.getCredential.mock.calls[1]?.[0].rpId, "accounts.example.com"); + + // Only the main frame runs the preload; anything else is not the page. + const forged = yield* call(PASSKEY_CREATE_CHANNEL, publicKey, { + origin: "https://evil.example", + }); + assert.deepStrictEqual(forged, { success: false, error: "NotAllowedError" }); + assert.strictEqual(webauthn.createCredential.mock.calls.length, 0); + + detach(); + assert.strictEqual(handlers.size, 0); + }).pipe(Effect.provide(bridgeLayer)), + ); + + it.effect("registers ES256 keys only and reports their real authenticator data", () => + Effect.gen(function* () { + const passkeys = yield* PreviewPasskeys.PreviewPasskeys; + const { guest, call } = makeGuest(); + passkeys.attachGuest(guest); + webauthn.createCredential.mockResolvedValue({ + success: true, + data: { + credentialId: "AQ", + clientDataJSON: "Ag", + attestationObject: attestationObject([1, 2, 3, 4]), + authData: Buffer.from('{"parsed":true}').toString("base64url"), + publicKey: "BQ", + publicKeyAlgorithm: -7, + transports: ["hybrid", "internal"], + extensions: {}, + }, + }); + const publicKey = { + challenge: new Uint8Array([1]), + pubKeyCredParams: [ + { type: "public-key", alg: -8 }, + { type: "public-key", alg: -7 }, + { type: "public-key", alg: -257 }, + ], + }; + + const result = yield* call(PASSKEY_CREATE_CHANNEL, publicKey); + + assert.deepStrictEqual(webauthn.createCredential.mock.calls[0]?.[0].pubKeyCredParams, [ + { type: "public-key", alg: -7 }, + ]); + assert.deepInclude(result, { success: true }); + const data = (result as { readonly data: Record }).data; + assert.strictEqual(data.authData, Buffer.from([1, 2, 3, 4]).toString("base64url")); + assert.deepStrictEqual(data.transports, []); + + const unsupported = yield* call(PASSKEY_CREATE_CHANNEL, { + ...publicKey, + pubKeyCredParams: [{ type: "public-key", alg: -8 }], + }); + assert.deepStrictEqual(unsupported, { success: false, error: "NotSupportedError" }); + assert.strictEqual(webauthn.createCredential.mock.calls.length, 1); + + // A malformed algorithm list is the page's error, not a cue to pick ES256. + const malformed = yield* call(PASSKEY_CREATE_CHANNEL, { + ...publicKey, + pubKeyCredParams: { type: "public-key", alg: -7 }, + }); + assert.deepStrictEqual(malformed, { success: false, error: "TypeError" }); + assert.strictEqual(webauthn.createCredential.mock.calls.length, 1); + + // Without authenticator data the site cannot verify the passkey. + webauthn.createCredential.mockResolvedValueOnce({ + success: true, + data: { credentialId: "AQ", attestationObject: Buffer.from([0x00]).toString("base64url") }, + }); + assert.deepStrictEqual(yield* call(PASSKEY_CREATE_CHANNEL, publicKey), { + success: false, + error: "NotAllowedError", + }); + }).pipe(Effect.provide(bridgeLayer)), + ); + + it.effect("answers only with a credential the site allowed", () => + Effect.gen(function* () { + const passkeys = yield* PreviewPasskeys.PreviewPasskeys; + const { guest, call } = makeGuest(); + passkeys.attachGuest(guest); + const assertion = (credentialId: string) => ({ + success: true, + data: { + credentialId, + clientDataJSON: "Ag", + authenticatorData: "Aw", + signature: "BA", + userHandle: "BQ", + }, + }); + const publicKey = { + challenge: new Uint8Array([1]), + rpId: "example.com", + allowCredentials: [{ type: "public-key", id: new Uint8Array([1]) }], + }; + + // macOS applies the allow list to passkeys but not to security keys. + webauthn.getCredential.mockResolvedValueOnce(assertion("Ag")); + const other = yield* call(PASSKEY_GET_CHANNEL, publicKey); + assert.deepStrictEqual(other, { success: false, error: "NotAllowedError" }); + + webauthn.getCredential.mockResolvedValueOnce(assertion("AQ")); + assert.deepInclude(yield* call(PASSKEY_GET_CHANNEL, publicKey), { success: true }); + + // With no allow list, any of the site's credentials will do. + webauthn.getCredential.mockResolvedValueOnce(assertion("Ag")); + const discoverable = yield* call(PASSKEY_GET_CHANNEL, { + challenge: new Uint8Array([1]), + rpId: "example.com", + }); + assert.deepInclude(discoverable, { success: true }); + }).pipe(Effect.provide(bridgeLayer)), + ); + + it.effect("opens the sheet only for a focused, secure page with nothing else pending", () => + Effect.gen(function* () { + const passkeys = yield* PreviewPasskeys.PreviewPasskeys; + const notAllowed = { success: false, error: "NotAllowedError" }; + const publicKey = { challenge: new Uint8Array([1]), rpId: "example.com" }; + + const lan = makeGuest("http://192.168.1.5:3000"); + passkeys.attachGuest(lan.guest); + assert.deepStrictEqual(yield* lan.call(PASSKEY_GET_CHANNEL, publicKey), notAllowed); + // macOS refuses `*.localhost`, so the page hears no rather than a native error. + const subdomain = makeGuest("http://app.localhost:3000"); + passkeys.attachGuest(subdomain.guest); + assert.deepStrictEqual(yield* subdomain.call(PASSKEY_GET_CHANNEL, publicKey), notAllowed); + + const page = makeGuest(); + passkeys.attachGuest(page.guest); + page.state.focused = false; + assert.deepStrictEqual(yield* page.call(PASSKEY_GET_CHANNEL, publicKey), notAllowed); + assert.strictEqual(webauthn.getCredential.mock.calls.length, 0); + + page.state.focused = true; + let finish: (value: unknown) => void = () => {}; + webauthn.getCredential.mockReturnValueOnce(new Promise((resolve) => (finish = resolve))); + const first = yield* Effect.forkChild(page.call(PASSKEY_GET_CHANNEL, publicKey)); + yield* Effect.promise(() => new Promise((resolve) => setImmediate(resolve))); + assert.deepStrictEqual(yield* page.call(PASSKEY_GET_CHANNEL, publicKey), notAllowed); + + // The page navigated while the sheet was up; its credential goes nowhere. + page.mainFrame.origin = "https://other.example"; + finish({ success: true, data: { credentialId: "AQ" } }); + assert.deepStrictEqual(yield* Fiber.join(first), notAllowed); + }).pipe(Effect.provide(bridgeLayer)), + ); + + it.effect("answers the page even when the native ceremony never settles", () => + Effect.gen(function* () { + const passkeys = yield* PreviewPasskeys.PreviewPasskeys; + const { guest, call } = makeGuest(); + passkeys.attachGuest(guest); + webauthn.getCredential.mockReturnValueOnce(new Promise(() => {})); + vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }); + + const pending = yield* Effect.forkChild( + call(PASSKEY_GET_CHANNEL, { challenge: new Uint8Array([1]), timeout: 1_000 }), + ); + yield* Effect.promise(() => vi.advanceTimersByTimeAsync(6_000)); + + assert.deepStrictEqual(yield* Fiber.join(pending), { + success: false, + error: "NotAllowedError", + }); + }).pipe(Effect.provide(bridgeLayer)), + ); + + it.effect("turns each path on only for builds signed for it", () => + Effect.gen(function* () { + const entitled = yield* PreviewPasskeys.PreviewPasskeys.pipe( + Effect.provide( + layerFor({ + t3codeWebAuthn: { + touchIdKeychainAccessGroup: "ABC1234567.com.t3tools.t3code.webauthn", + browserPasskeys: false, + }, + }), + ), + ); + yield* entitled.configure; + assert.deepStrictEqual(electron.configureWebAuthn.mock.calls, [ + [{ touchID: { keychainAccessGroup: "ABC1234567.com.t3tools.t3code.webauthn" } }], + ]); + assert.isFalse(entitled.bridgeEnabled); + const { guest, handlers } = makeGuest(); + entitled.attachGuest(guest); + assert.strictEqual(handlers.size, 0); + + const unpackaged = yield* PreviewPasskeys.PreviewPasskeys.pipe( + Effect.provide( + layerFor( + { t3codeWebAuthn: { touchIdKeychainAccessGroup: "X", browserPasskeys: true } }, + false, + ), + ), + ); + yield* unpackaged.configure; + assert.strictEqual(electron.configureWebAuthn.mock.calls.length, 1); + assert.isFalse(unpackaged.bridgeEnabled); + }), + ); + + it.effect("asks which passkey to use only when a site has several", () => + Effect.gen(function* () { + const passkeys = yield* PreviewPasskeys.PreviewPasskeys; + const listeners: Array<(...args: ReadonlyArray) => void> = []; + const session = { + on: (_event: string, listener: (...args: ReadonlyArray) => void) => + listeners.push(listener), + } as unknown as Electron.Session; + passkeys.installSessionHandlers(session); + passkeys.installSessionHandlers(session); + assert.strictEqual(listeners.length, 1); + + const select = (accounts: ReadonlyArray) => + Effect.promise( + () => + new Promise((resolve) => + listeners[0]?.( + {}, + { relyingPartyId: "example.com", accounts, frame: null }, + (credentialId?: string) => resolve(credentialId), + ), + ), + ); + const alice = { credentialId: "alice", name: "alice@example.com" }; + const bob = { credentialId: "bob", displayName: "Bob", name: "bob@example.com" }; + + assert.strictEqual(yield* select([alice]), "alice"); + assert.strictEqual(electron.showMessageBox.mock.calls.length, 0); + + electron.showMessageBox.mockResolvedValueOnce({ response: 1 }); + assert.strictEqual(yield* select([alice, bob]), "bob"); + assert.deepStrictEqual(electron.showMessageBox.mock.calls[0]?.[0]?.buttons, [ + "alice@example.com", + "Bob (bob@example.com)", + "Cancel", + ]); + + electron.showMessageBox.mockResolvedValueOnce({ response: 2 }); + assert.isUndefined(yield* select([alice, bob])); + }).pipe(Effect.provide(layerFor({}))), + ); +}); diff --git a/apps/desktop/src/preview/Passkeys.ts b/apps/desktop/src/preview/Passkeys.ts new file mode 100644 index 000000000000..c06b4dc522b5 --- /dev/null +++ b/apps/desktop/src/preview/Passkeys.ts @@ -0,0 +1,242 @@ +// @effect-diagnostics globalTimers:off - Ceremonies settle inside IPC handlers, outside an Effect runtime. +import type { IpcMainInvokeEvent, Session, WebContents, WebFrameMain } from "electron"; +import { app, BrowserWindow, dialog, webContents as electronWebContents } from "electron"; +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 Schema from "effect/Schema"; + +import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; +import { PASSKEY_CREATE_CHANNEL, PASSKEY_GET_CHANNEL } from "./GuestProtocol.ts"; +import type { PasskeyBackend, PasskeyCeremonyContext } from "./PasskeyBackend.ts"; +import { macPasskeyBackend } from "./PasskeyBackendMac.ts"; +import type { PasskeyCeremonyResult } from "./PasskeyBridge.ts"; + +const PasskeyPackageMetadata = Schema.Struct({ + t3codeWebAuthn: Schema.optional( + Schema.Struct({ + touchIdKeychainAccessGroup: Schema.optional(Schema.String), + browserPasskeys: Schema.optional(Schema.Boolean), + }), + ), +}); +const decodePasskeyPackageMetadata = Schema.decodeEffect( + Schema.fromJsonString(PasskeyPackageMetadata), +); + +export class PreviewPasskeysConfigureError extends Schema.TaggedError()( + "PreviewPasskeysConfigureError", + { + keychainAccessGroup: Schema.String, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to enable Touch ID passkeys for the in-app browser (keychain group ${this.keychainAccessGroup}).`; + } +} + +/** + * Passkeys for in-app browser pages on macOS, where Electron has no WebAuthn UI + * of its own. Signed builds record which entitlements their provisioning + * profile granted (scripts/build-desktop-artifact.ts), and each path turns on + * only when its entitlement is present: + * + * - Touch ID: Electron's built-in platform authenticator. Passkeys it creates + * stay on this Mac, in T3 Code's keychain group. + * - Browser passkeys: Apple's managed browser entitlement lets the system sheet + * (iCloud Keychain, password managers, phones, security keys) serve any + * site. The guest preload hands each ceremony to this process, which pins + * the requesting frame's real origin. + * + * Windows needs neither: Chromium already uses Windows Hello there. + */ +export class PreviewPasskeys extends Context.Service< + PreviewPasskeys, + { + /** Whether preview pages route WebAuthn through the system passkey sheet. */ + readonly bridgeEnabled: boolean; + /** Turns on Touch ID passkeys when this build is entitled to them. Runs after app ready. */ + readonly configure: Effect.Effect; + /** Lets the user choose between several passkeys for one site. Idempotent. */ + readonly installSessionHandlers: (session: Session) => void; + /** Serves a preview guest's passkey ceremonies. Returns the detach function. */ + readonly attachGuest: (guest: WebContents) => () => void; + } +>()("@t3tools/desktop/preview/Passkeys/PreviewPasskeys") {} + +const notAllowed: PasskeyCeremonyResult = { success: false, error: "NotAllowedError" }; + +const accountLabel = (account: Electron.WebAuthnAccount) => + account.displayName && account.name && account.displayName !== account.name + ? `${account.displayName} (${account.name})` + : (account.name ?? account.displayName ?? "Unnamed passkey"); + +const ownerWindow = (frame: WebFrameMain | null) => { + const contents = frame ? electronWebContents.fromFrame(frame) : undefined; + const owner = contents?.hostWebContents ?? contents; + return owner ? BrowserWindow.fromWebContents(owner) : null; +}; + +const chooseAccount = async (details: Electron.SelectWebauthnAccountDetails) => { + const [onlyAccount] = details.accounts; + // The Touch ID prompt that follows already asks the user to confirm. + if (details.accounts.length === 1) return onlyAccount?.credentialId; + const options: Electron.MessageBoxOptions = { + type: "none", + message: "Choose a passkey", + detail: details.relyingPartyId, + buttons: [...details.accounts.map(accountLabel), "Cancel"], + cancelId: details.accounts.length, + defaultId: 0, + noLink: true, + }; + const parent = ownerWindow(details.frame); + const { response } = parent + ? await dialog.showMessageBox(parent, options) + : await dialog.showMessageBox(options); + return details.accounts[response]?.credentialId; +}; + +const NATIVE_DEFAULT_TIMEOUT_MS = 10 * 60 * 1000; +const NATIVE_MAX_TIMEOUT_MS = 60 * 60 * 1000; + +// Mirrors the native layer's own clamp, plus a margin: it can leave a ceremony +// unsettled, and the page must still get an answer. +const ceremonyDeadline = (timeout: unknown) => + (typeof timeout === "number" && timeout > 0 + ? Math.min(timeout, NATIVE_MAX_TIMEOUT_MS) + : NATIVE_DEFAULT_TIMEOUT_MS) + 5_000; + +const withDeadline = (ceremony: Promise, milliseconds: number) => { + let timer: ReturnType | undefined; + const deadline = new Promise((resolve) => { + timer = setTimeout(() => resolve(notAllowed), milliseconds); + }); + return Promise.race([ceremony, deadline]).finally(() => clearTimeout(timer)); +}; + +// Secure contexts only: https, or http on this machine. Chromium also trusts +// `*.localhost`, but the macOS layer refuses it, so it is turned away here +// instead of failing deeper with a less useful error. +const isTrustworthyOrigin = (origin: string) => { + if (!URL.canParse(origin)) return false; + const { protocol, hostname } = new URL(origin); + return ( + protocol === "https:" || + (protocol === "http:" && + (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]")) + ); +}; + +/** + * The platform that serves preview pages' passkey ceremonies, or none. Windows + * needs none: Chromium already hands WebAuthn to Windows Hello there. + */ +const backendFor = (platform: NodeJS.Platform): PasskeyBackend | undefined => + platform === "darwin" ? macPasskeyBackend : undefined; + +const make = Effect.gen(function* () { + const environment = yield* DesktopEnvironment.DesktopEnvironment; + const fileSystem = yield* FileSystem.FileSystem; + + const metadata = + environment.platform === "darwin" && environment.isPackaged + ? yield* fileSystem + .readFileString(environment.path.join(environment.appRoot, "package.json")) + .pipe( + Effect.flatMap(decodePasskeyPackageMetadata), + Effect.map((parsed) => parsed.t3codeWebAuthn), + Effect.orElseSucceed(() => undefined), + ) + : undefined; + const keychainAccessGroup = metadata?.touchIdKeychainAccessGroup; + const backend = metadata?.browserPasskeys === true ? backendFor(environment.platform) : undefined; + const bridgeEnabled = backend !== undefined; + const sessionsWithHandlers = new WeakSet(); + + return PreviewPasskeys.of({ + bridgeEnabled, + configure: Effect.gen(function* () { + if (keychainAccessGroup === undefined) return; + yield* Effect.try({ + try: () => app.configureWebAuthn({ touchID: { keychainAccessGroup } }), + catch: (cause) => new PreviewPasskeysConfigureError({ keychainAccessGroup, cause }), + }).pipe(Effect.catch((error) => Effect.logWarning(error.message, { cause: error.cause }))); + }).pipe(Effect.withSpan("desktop.previewPasskeys.configure")), + installSessionHandlers: (session) => { + if (sessionsWithHandlers.has(session)) return; + sessionsWithHandlers.add(session); + session.on("select-webauthn-account", (_event, details, callback) => { + // The request stays pending until the callback runs, so it must run once. + void chooseAccount(details).then( + (credentialId) => callback(credentialId), + () => callback(), + ); + }); + }, + attachGuest: (guest) => { + if (backend === undefined) return () => {}; + let ceremonyPending = false; + const serve = async ( + event: IpcMainInvokeEvent, + publicKey: { readonly timeout?: unknown } | undefined, + ceremony: (context: PasskeyCeremonyContext) => Promise, + ): Promise => { + // Only the guest's main frame runs the bridge preload. + const frame = event.senderFrame; + if (!frame || frame !== guest.mainFrame) return notAllowed; + // The page shares a JS world with the preload, so nothing it sends can + // be trusted for the origin. Read the committed origin before any await: + // the frame object outlives a navigation. + const origin = frame.origin; + // Like Chromium, only a focused page may open the sheet, one at a time. + if (!isTrustworthyOrigin(origin) || ceremonyPending || !guest.isFocused()) { + return notAllowed; + } + if (typeof publicKey !== "object" || publicKey === null) { + return { success: false, error: "TypeError" }; + } + const host = guest.hostWebContents + ? BrowserWindow.fromWebContents(guest.hostWebContents) + : null; + if (!host || host.isDestroyed()) return notAllowed; + + ceremonyPending = true; + try { + const result = await withDeadline( + ceremony({ origin, nativeWindowHandle: host.getNativeWindowHandle() }), + ceremonyDeadline(publicKey.timeout), + ); + // A credential minted for a document that navigated away belongs to nobody. + const sameDocument = + !frame.isDestroyed() && frame === guest.mainFrame && frame.origin === origin; + return sameDocument ? result : notAllowed; + } catch { + return notAllowed; + } finally { + ceremonyPending = false; + } + }; + const detach = () => { + guest.ipc.removeHandler(PASSKEY_CREATE_CHANNEL); + guest.ipc.removeHandler(PASSKEY_GET_CHANNEL); + }; + detach(); + // Option shapes are the page's to get wrong: the native layer validates + // them and answers with a TypeError. + guest.ipc.handle( + PASSKEY_CREATE_CHANNEL, + (event, publicKey: PublicKeyCredentialCreationOptions) => + serve(event, publicKey, (context) => backend.create(publicKey, context)), + ); + guest.ipc.handle(PASSKEY_GET_CHANNEL, (event, publicKey: PublicKeyCredentialRequestOptions) => + serve(event, publicKey, (context) => backend.get(publicKey, context)), + ); + return detach; + }, + }); +}).pipe(Effect.withSpan("PreviewPasskeys.make")); + +export const layer = Layer.effect(PreviewPasskeys, make); diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index 10c3cb8adee9..9e4ed5ca434a 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -56,6 +56,7 @@ import { import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts"; import * as DesktopWindow from "./DesktopWindow.ts"; import * as PreviewManager from "../preview/Manager.ts"; +import * as PreviewPasskeys from "../preview/Passkeys.ts"; const environmentInput = { dirname: "/repo/apps/desktop/dist-electron", @@ -325,6 +326,11 @@ function layerTest(input: { } satisfies ElectronShell.ElectronShell["Service"]), layerElectronTheme, layerElectronWindow, + Layer.mock(PreviewPasskeys.PreviewPasskeys)({ + bridgeEnabled: false, + installSessionHandlers: () => {}, + attachGuest: () => () => {}, + }), Layer.mock(PreviewManager.PreviewManager)({ getBrowserSession: () => Effect.succeed({} as Electron.Session), setMainWindow: () => Effect.void, @@ -433,6 +439,11 @@ const makeSplashScenario = (createOutcomes: readonly (Electron.BrowserWindow | n } satisfies ElectronShell.ElectronShell["Service"]), layerElectronTheme, Layer.succeed(ElectronWindow.ElectronWindow, electronWindowShape), + Layer.mock(PreviewPasskeys.PreviewPasskeys)({ + bridgeEnabled: false, + installSessionHandlers: () => {}, + attachGuest: () => () => {}, + }), Layer.mock(PreviewManager.PreviewManager)({ getBrowserSession: () => Effect.succeed({} as Electron.Session), setMainWindow: () => Effect.void, diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index 6dede819356a..07942b67356b 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -25,7 +25,9 @@ import { TRACKPAD_SCROLL_END_CHANNEL, WINDOW_FULLSCREEN_STATE_CHANNEL, } from "../ipc/channels.ts"; +import { PASSKEY_BRIDGE_ARGUMENT } from "../preview/GuestProtocol.ts"; import * as PreviewManager from "../preview/Manager.ts"; +import * as PreviewPasskeys from "../preview/Passkeys.ts"; import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts"; import * as DesktopClientSettings from "../settings/DesktopClientSettings.ts"; import * as ElectronApp from "../electron/ElectronApp.ts"; @@ -326,6 +328,7 @@ export const make = Effect.gen(function* () { const electronTheme = yield* ElectronTheme.ElectronTheme; const electronWindow = yield* ElectronWindow.ElectronWindow; const previewManager = yield* PreviewManager.PreviewManager; + const previewPasskeys = yield* PreviewPasskeys.PreviewPasskeys; const desktopSettings = yield* DesktopAppSettings.DesktopAppSettings; const clientSettings = yield* DesktopClientSettings.DesktopClientSettings; const electronApp = yield* ElectronApp.ElectronApp; @@ -537,6 +540,12 @@ export const make = Effect.gen(function* () { webPreferences.nodeIntegrationInSubFrames = false; webPreferences.contextIsolation = false; webPreferences.disableHtmlFullscreenWindowResize = true; + if (previewPasskeys.bridgeEnabled) { + webPreferences.additionalArguments = [ + ...(webPreferences.additionalArguments ?? []), + PASSKEY_BRIDGE_ARGUMENT, + ]; + } }); const contextMenuContents = new WeakSet(); diff --git a/docs/operations/release.md b/docs/operations/release.md index baff2ed56b23..6be0d8fd90fe 100644 --- a/docs/operations/release.md +++ b/docs/operations/release.md @@ -553,6 +553,16 @@ Notes: - The workflow writes it to a temporary `AuthKey_.p8` file at runtime. - The workflow decodes `MACOS_PROVISIONING_PROFILE`, validates it with `security cms`, and passes it to the desktop packager. +- In-app browser passkeys depend on the same profile. The packager adds each entitlement only when + the profile grants it, because macOS will not launch an app that claims more than its profile + allows. The build log reports which ones it enabled. + - `keychain-access-groups` (`.com.t3tools.t3code.webauthn`) enables Touch ID passkeys. + Profiles that grant the team's keychain groups (`.*`) cover it. + - `com.apple.developer.web-browser.public-key-credential` lets the system passkey sheet (iCloud + Keychain, password managers, phones, security keys) serve any site. Apple grants it as a + managed capability: the Account Holder requests it through the + [macOS Browsers Passkeys form](https://developer.apple.com/contact/request/macos-browsers-passkeys/). + After approval, regenerate the profile and update `MACOS_PROVISIONING_PROFILE`. ## 3) Azure Trusted Signing setup (Windows) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a74905c8cd80..5790c62cc13d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,12 @@ catalogs: '@pierre/diffs': specifier: 1.5.2 version: 1.5.2 + '@types/plist': + specifier: 3.0.5 + version: 3.0.5 + electron-webauthn: + specifier: 1.3.1 + version: 1.3.1 jose: specifier: 6.2.2 version: 6.2.2 @@ -42,6 +48,9 @@ catalogs: micromark-util-types: specifier: 2.0.2 version: 2.0.2 + plist: + specifier: 3.1.1 + version: 3.1.1 rehype-raw: specifier: 7.0.0 version: 7.0.0 @@ -60,6 +69,9 @@ catalogs: remark-rehype: specifier: 11.1.2 version: 11.1.2 + tldts: + specifier: 7.4.2 + version: 7.4.2 typescript: specifier: 7.0.2 version: 7.0.2 @@ -117,6 +129,8 @@ overrides: '@opencode/protocol>effect': 4.0.2 '@opencode/schema>effect': 4.0.2 node-abi: 4.33.0 + electron-webauthn>@electron-webauthn/macos: 1.3.1 + '@electron-webauthn/macos>objc-js': 1.5.0 lightningcss: 1.33.0 tailwindcss: 4.3.3 '@types/ws@^8': 8.18.2 @@ -229,6 +243,9 @@ importers: electron-updater: specifier: ^6.8.9 version: 6.8.9 + electron-webauthn: + specifier: 'catalog:' + version: 1.3.1(typescript@7.0.2) ffi-rs: specifier: 1.3.2 version: 1.3.2 @@ -238,6 +255,9 @@ importers: react-grab: specifier: ^0.1.32 version: 0.1.44(react@19.2.6) + tldts: + specifier: 'catalog:' + version: 7.4.2 devDependencies: '@effect/vitest': specifier: 4.0.2 @@ -1554,6 +1574,9 @@ importers: effect: specifier: 4.0.2 version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) + plist: + specifier: 'catalog:' + version: 3.1.1 pngjs: specifier: 7.0.0 version: 7.0.0 @@ -1564,6 +1587,9 @@ importers: '@effect/vitest': specifier: 4.0.2 version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + '@types/plist': + specifier: 'catalog:' + version: 3.0.5 '@types/pngjs': specifier: 6.0.5 version: 6.0.5 @@ -2868,6 +2894,15 @@ packages: resolution: {integrity: sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==} engines: {node: '>=22.12.0'} + '@electron-webauthn/macos@1.3.1': + resolution: {integrity: sha512-NJA4I83ulh3pOfkv7sBkEGecTxaCtjFUbGTAi+BLNWdFc463AY7DaUprsqbGr+WAnD8SIUpuHSO7vC0C+HPu6g==} + os: [darwin] + peerDependencies: + typescript: ^6.0.2 + + '@electron-webauthn/types@1.3.1': + resolution: {integrity: sha512-C7Jy6dg32/QzIAl1E110acjpp6+b67xqZJL4L0rL2X2VIOJx4m+vfcWl3AjC4JgkxUBcSMSNa85bay2/473y/Q==} + '@electron/asar@3.4.1': resolution: {integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==} engines: {node: '>=10.12.0'} @@ -4215,9 +4250,32 @@ packages: '@opencode/schema@2.0.23': resolution: {integrity: sha512-DglA+CCNmCaiAJ8FqAKo2EKIkCOF9/EmMEYowcXhTrQcoJ7eaTx7ytGjPgis6VHQuU4aaAgwZYw96ypS3ojsIw==} + '@oslojs/asn1@1.0.0': + resolution: {integrity: sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + '@oslojs/binary@1.0.0': + resolution: {integrity: sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + '@oslojs/cbor@1.0.0': + resolution: {integrity: sha512-AY6Lknexs7n2xp8Cgey95c+975VG7XOk4UEdRdNFxHmDDbuf47OC/LAVRsl14DeTLwo8W6xr3HLFwUFmKcndTQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + '@oslojs/crypto@1.0.0': + resolution: {integrity: sha512-dVz8TkkgYdr3tlwxHd7SCYGxoN7ynwHLA0nei/Aq9C+ERU0BK+U8+/3soEzBUxUNKYBf42351DyJUZ2REla50w==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + '@oslojs/encoding@1.0.0': + resolution: {integrity: sha512-dyIB0SdZgMm5BhGwdSp8rMxEFIopLKxDG1vxIBaiogyom6ZqH2aXPb6DEC2WzOOWKdPSq1cxdNeRx2wAn1Z+ZQ==} + '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + '@oslojs/webauthn@1.0.0': + resolution: {integrity: sha512-2ZRpbt3msNURwvjmavzq9vrNlxUnWFBGMYqbC1kO3fYBLskL7r4DiLJT1wbtLoI+hclFwjhl48YhRFBl6RWg1A==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + '@oxc-parser/binding-android-arm-eabi@0.147.0': resolution: {integrity: sha512-fOtoGvIoirkvxQVw9J1WJPxz571XPgLsPf9uhRD+PJteUnvrJHMDmK9pw2yZEGGyismtRoEsp+JcXUdF/JDMDw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6049,6 +6107,9 @@ packages: '@types/node@24.12.4': resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==} + '@types/plist@3.0.5': + resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} + '@types/pngjs@6.0.5': resolution: {integrity: sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==} @@ -7991,6 +8052,11 @@ packages: electron-updater@6.8.9: resolution: {integrity: sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==} + electron-webauthn@1.3.1: + resolution: {integrity: sha512-I8/SOjPfnIiVjN38eTX3BRZfuFDGzLWtlByh4OecmlVkfxQt9R/zCwEm4PquyvaqP+xtGgkFrBxwOuJ+DhWlDg==} + peerDependencies: + typescript: ^6.0.2 + electron-winstaller@5.4.0: resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} engines: {node: '>=8.0.0'} @@ -9813,6 +9879,10 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-addon-api@8.9.2: + resolution: {integrity: sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==} + engines: {node: ^18 || ^20 || >= 21} + node-api-version@0.2.1: resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==} @@ -9908,6 +9978,10 @@ packages: resolution: {integrity: sha512-i8iA8uij0g1YQzS8uOJPSRCgwDjO9warIHUAu1Fqj877Wc3wlfxDYBioYWgKTBF2+URVJttyDWSEpmd99nlvtQ==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + objc-js@1.5.0: + resolution: {integrity: sha512-IHmouX5xYrE2UyGRRlpwY7u2HJVjF6P/P+jy6Vqga+Ya7RAf/hFeNkt4KgTkGUMM3VmnBVUEMJ0tKWY2RlYgKg==} + os: [darwin] + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -13599,6 +13673,16 @@ snapshots: '@electron-internal/extract-zip@1.0.5': {} + '@electron-webauthn/macos@1.3.1(typescript@7.0.2)': + dependencies: + '@electron-webauthn/types': 1.3.1 + '@oslojs/webauthn': 1.0.0 + objc-js: 1.5.0 + typescript: 7.0.2 + optional: true + + '@electron-webauthn/types@1.3.1': {} + '@electron/asar@3.4.1': dependencies: commander: 5.1.0 @@ -14991,8 +15075,39 @@ snapshots: '@standard-schema/spec': 1.1.0 effect: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) + '@oslojs/asn1@1.0.0': + dependencies: + '@oslojs/binary': 1.0.0 + optional: true + + '@oslojs/binary@1.0.0': + optional: true + + '@oslojs/cbor@1.0.0': + dependencies: + '@oslojs/binary': 1.0.0 + optional: true + + '@oslojs/crypto@1.0.0': + dependencies: + '@oslojs/asn1': 1.0.0 + '@oslojs/binary': 1.0.0 + optional: true + + '@oslojs/encoding@1.0.0': + optional: true + '@oslojs/encoding@1.1.0': {} + '@oslojs/webauthn@1.0.0': + dependencies: + '@oslojs/asn1': 1.0.0 + '@oslojs/binary': 1.0.0 + '@oslojs/cbor': 1.0.0 + '@oslojs/crypto': 1.0.0 + '@oslojs/encoding': 1.0.0 + optional: true + '@oxc-parser/binding-android-arm-eabi@0.147.0': optional: true @@ -16589,6 +16704,11 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/plist@3.0.5': + dependencies: + '@types/node': 24.12.4 + xmlbuilder: 15.1.1 + '@types/pngjs@6.0.5': dependencies: '@types/node': 24.12.4 @@ -18436,6 +18556,13 @@ snapshots: transitivePeerDependencies: - supports-color + electron-webauthn@1.3.1(typescript@7.0.2): + dependencies: + '@electron-webauthn/types': 1.3.1 + typescript: 7.0.2 + optionalDependencies: + '@electron-webauthn/macos': 1.3.1(typescript@7.0.2) + electron-winstaller@5.4.0: dependencies: '@electron/asar': 3.4.1 @@ -20739,6 +20866,9 @@ snapshots: node-addon-api@7.1.1: {} + node-addon-api@8.9.2: + optional: true + node-api-version@0.2.1: dependencies: semver: 7.8.5 @@ -20857,6 +20987,12 @@ snapshots: dependencies: flow-enums-runtime: 0.0.6 + objc-js@1.5.0: + dependencies: + node-addon-api: 8.9.2 + node-gyp-build: 4.8.4 + optional: true + object-assign@4.1.1: {} object-inspect@1.13.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 22e0b07249eb..59358907b7c4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -18,6 +18,8 @@ allowBuilds: msgpackr-extract: true msw: false node-pty: true + # Ships N-API prebuilds; its install script only rebuilds from source. + objc-js: false sharp: true utf-8-validate: false workerd: false @@ -38,6 +40,7 @@ catalog: "@effect/sql-sqlite-do": 4.0.2 "@effect/tsgo": 0.46.1 "@effect/vitest": 4.0.2 + "@electron-webauthn/macos": 1.3.1 "@legendapp/list": 3.3.5 "@noble/curves": 1.9.1 "@noble/hashes": 1.8.0 @@ -46,11 +49,16 @@ catalog: "@tailwindcss/oxide": 4.3.3 "@tailwindcss/vite": 4.3.3 "@types/node": 24.12.4 + "@types/plist": 3.0.5 "@types/ws": 8.18.2 effect: 4.0.2 + electron-webauthn: 1.3.1 jose: 6.2.2 lightningcss: 1.33.0 + objc-js: 1.5.0 + plist: 3.1.1 tailwindcss: 4.3.3 + tldts: 7.4.2 typescript: 7.0.2 undici: 8.11.2 vite: npm:@voidzero-dev/vite-plus-core@1.0.0 @@ -249,6 +257,10 @@ overrides: "@opencode/protocol>effect": "catalog:" "@opencode/schema>effect": "catalog:" node-abi: 4.33.0 + # The desktop app installs these without a lockfile when it packages them, so + # pin the native passkey code that runs in its main process. + "electron-webauthn>@electron-webauthn/macos": "catalog:" + "@electron-webauthn/macos>objc-js": "catalog:" lightningcss: "catalog:" tailwindcss: "catalog:" # Ranges from @effect/platform-node, jsdom, unifont, Expo and libsql otherwise diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index 4b9294e16887..2fc9adf94526 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -44,6 +44,7 @@ import { preflightMacDesktopBuild, preflightWindowsDesktopBuild, renderMacPasskeyEntitlements, + resolveMacWebAuthnEntitlements, resolveClerkPasskeyNativeArtifacts, resolveMacPasskeySigningConfiguration, resolveDesktopRuntimeDependencies, @@ -95,6 +96,12 @@ import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts"; import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; +// Keeps pnpm from auto-installing TypeScript, a types-only peer, into the app. +const stagePackageExtensions = { + "electron-webauthn": { peerDependenciesMeta: { typescript: { optional: true } } }, + "@electron-webauthn/macos": { peerDependenciesMeta: { typescript: { optional: true } } }, +}; + // A minimal stand-in for the Linux CLI release archive: one top-level // directory named after the archive stem holding the executable, the web // client, and the runtime externals with node-pty built from source. @@ -454,6 +461,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { os: ["darwin"], cpu: ["x64"], }, + packageExtensions: stagePackageExtensions, }); assert.deepStrictEqual(createStageWorkspaceConfig({ platform: "linux", arch: "x64" }), { supportedArchitectures: { @@ -461,6 +469,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { cpu: ["x64"], libc: ["glibc"], }, + packageExtensions: stagePackageExtensions, }); // Windows stages only win32 natives; WSL runs the separately built Linux // CLI archive rather than anything installed here. @@ -469,18 +478,21 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { os: ["win32"], cpu: ["x64"], }, + packageExtensions: stagePackageExtensions, }); assert.deepStrictEqual(createStageWorkspaceConfig({ platform: "win", arch: "arm64" }), { supportedArchitectures: { os: ["win32"], cpu: ["arm64"], }, + packageExtensions: stagePackageExtensions, }); assert.deepStrictEqual(createStageWorkspaceConfig({ platform: "mac", arch: "universal" }), { supportedArchitectures: { os: ["darwin"], cpu: ["arm64", "x64"], }, + packageExtensions: stagePackageExtensions, }); }); @@ -507,6 +519,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { cpu: ["x64"], libc: ["glibc"], }, + packageExtensions: stagePackageExtensions, allowBuilds: { electron: true, "node-pty": true, @@ -537,6 +550,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { os: ["darwin"], cpu: ["arm64"], }, + packageExtensions: stagePackageExtensions, }, ); }); @@ -1886,7 +1900,10 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { T3CODE_CLERK_PASSKEY_RP_DOMAINS: " Clerk.Example.com,example.clerk.accounts.dev,clerk.example.com ", }); - const entitlements = renderMacPasskeyEntitlements(configuration); + const entitlements = renderMacPasskeyEntitlements(configuration, { + touchIdKeychainAccessGroup: undefined, + browserPasskeys: false, + }); assert.deepStrictEqual(configuration.rpDomains, [ "clerk.example.com", @@ -1896,6 +1913,78 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { assert.include(entitlements, "webcredentials:clerk.example.com"); assert.include(entitlements, "webcredentials:example.clerk.accounts.dev"); assert.include(entitlements, "com.apple.security.cs.allow-jit"); + assert.notInclude(entitlements, "keychain-access-groups"); + assert.notInclude(entitlements, "com.apple.developer.web-browser.public-key-credential"); + }); + + it("grants in-app browser passkey entitlements only when the provisioning profile does", () => { + const configuration = { appId: "com.t3tools.t3code", teamId: "ABC1234567" }; + // Profiles are CMS envelopes around a plain XML plist. + const profile = (entitlements: string, outside = "") => + `0\x82\x1f\x9a\x06\t*\x86H${outside}Entitlements${entitlements}\x00\x01`; + const teamWildcardGroups = `keychain-access-groups + + ABC1234567.* + com.apple.token + `; + + assert.deepStrictEqual(resolveMacWebAuthnEntitlements(profile(""), configuration), { + touchIdKeychainAccessGroup: undefined, + browserPasskeys: false, + }); + assert.deepStrictEqual( + resolveMacWebAuthnEntitlements( + profile("keychain-access-groupsOTHERTEAM1.*"), + configuration, + ), + { touchIdKeychainAccessGroup: undefined, browserPasskeys: false }, + ); + // Only real values inside the Entitlements dict count: not comments, not + // explicit false, not keys elsewhere in the profile. + assert.deepStrictEqual( + resolveMacWebAuthnEntitlements( + profile( + ` + com.apple.developer.web-browser.public-key-credential`, + teamWildcardGroups, + ), + configuration, + ), + { touchIdKeychainAccessGroup: undefined, browserPasskeys: false }, + ); + assert.deepStrictEqual( + resolveMacWebAuthnEntitlements( + profile( + `${teamWildcardGroups} + com.apple.developer.web-browser.public-key-credential + `, + ), + configuration, + ), + { + touchIdKeychainAccessGroup: "ABC1234567.com.t3tools.t3code.webauthn", + browserPasskeys: true, + }, + ); + + const entitlements = renderMacPasskeyEntitlements( + { ...configuration, rpDomains: ["clerk.example.com"], provisioningProfilePath: "" }, + resolveMacWebAuthnEntitlements( + profile( + `${teamWildcardGroups}com.apple.developer.web-browser.public-key-credential`, + ), + configuration, + ), + ); + assert.match( + entitlements, + /keychain-access-groups<\/key>\s*\s*ABC1234567\.com\.t3tools\.t3code\.webauthn<\/string>\s*<\/array>/u, + ); + assert.match( + entitlements, + /com\.apple\.developer\.web-browser\.public-key-credential<\/key>\s*/u, + ); }); it("rejects incomplete macOS passkey signing configuration", () => { diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 301b42613d25..ed14f57b61a4 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -4,6 +4,8 @@ import * as NodeFSP from "node:fs/promises"; import * as NodeCrypto from "node:crypto"; import * as NodeModule from "node:module"; +// plist is CommonJS; Node cannot load its named exports from an ES module. +import Plist from "plist"; import { createPackageWithOptions, @@ -80,10 +82,28 @@ const StageWorkspaceConfig = Schema.Struct({ allowBuilds: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)), patchedDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)), overrides: Schema.optional(Schema.Record(Schema.String, Schema.String)), + packageExtensions: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + peerDependenciesMeta: Schema.Record( + Schema.String, + Schema.Struct({ optional: Schema.Boolean }), + ), + }), + ), + ), nodeLinker: Schema.optional(Schema.Literals(["hoisted"])), }); type StageWorkspaceConfig = typeof StageWorkspaceConfig.Type; +// electron-webauthn declares TypeScript as a peer only for its typings. pnpm +// auto-installs missing peers, which would ship a compiler inside the app. +const STAGE_PACKAGE_EXTENSIONS = { + "electron-webauthn": { peerDependenciesMeta: { typescript: { optional: true } } }, + "@electron-webauthn/macos": { peerDependenciesMeta: { typescript: { optional: true } } }, +} as const; + const RepoRoot = Effect.service(Path.Path).pipe( Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))), ); @@ -926,6 +946,7 @@ interface StagePackageJson { readonly version: string; readonly buildVersion: string; readonly t3codeCommitHash: string; + readonly t3codeWebAuthn?: MacWebAuthnEntitlements; readonly private: true; readonly packageManager: string; readonly description: string; @@ -1289,12 +1310,80 @@ function escapeXml(value: string): string { .replaceAll("'", "'"); } +/** + * Passkey entitlements for the in-app browser. Each is granted only when the + * provisioning profile authorizes it: macOS refuses to launch an app that + * claims a restricted entitlement its embedded profile does not carry. + */ +export interface MacWebAuthnEntitlements { + /** Keychain group for Electron's Touch ID passkeys. */ + readonly touchIdKeychainAccessGroup: string | undefined; + /** Apple's managed browser entitlement, which allows passkeys for any site. */ + readonly browserPasskeys: boolean; +} + +const BROWSER_PASSKEYS_ENTITLEMENT = "com.apple.developer.web-browser.public-key-credential"; + +const ProvisioningProfilePlist = Schema.Struct({ + Entitlements: Schema.Struct({ + "keychain-access-groups": Schema.optional(Schema.Array(Schema.String)), + [BROWSER_PASSKEYS_ENTITLEMENT]: Schema.optional(Schema.Boolean), + }), +}); +const isProvisioningProfilePlist = Schema.is(ProvisioningProfilePlist); + +/** + * Reads the Entitlements dict of the XML plist a provisioning profile wraps in + * its CMS envelope. Anything unreadable grants nothing. + */ +const readProfileEntitlements = (provisioningProfile: string) => { + const start = provisioningProfile.indexOf("", start); + if (start === -1 || end === -1) return undefined; + try { + const profile: unknown = Plist.parse(provisioningProfile.slice(start, end + "".length)); + return isProvisioningProfilePlist(profile) ? profile.Entitlements : undefined; + } catch { + return undefined; + } +}; + +export function resolveMacWebAuthnEntitlements( + provisioningProfile: string, + configuration: Pick, +): MacWebAuthnEntitlements { + const entitlements = readProfileEntitlements(provisioningProfile); + const keychainAccessGroup = `${configuration.teamId}.${configuration.appId}.webauthn`; + const keychainGroupAuthorized = (entitlements?.["keychain-access-groups"] ?? []).some((group) => + group.endsWith("*") + ? keychainAccessGroup.startsWith(group.slice(0, -1)) + : group === keychainAccessGroup, + ); + return { + touchIdKeychainAccessGroup: keychainGroupAuthorized ? keychainAccessGroup : undefined, + browserPasskeys: entitlements?.[BROWSER_PASSKEYS_ENTITLEMENT] === true, + }; +} + export function renderMacPasskeyEntitlements( configuration: MacPasskeySigningConfiguration, + webAuthn: MacWebAuthnEntitlements, ): string { const associatedDomains = configuration.rpDomains .map((domain) => ` webcredentials:${escapeXml(domain)}`) .join("\n"); + const keychainAccessGroups = webAuthn.touchIdKeychainAccessGroup + ? ` + keychain-access-groups + + ${escapeXml(webAuthn.touchIdKeychainAccessGroup)} + ` + : ""; + const browserPasskeys = webAuthn.browserPasskeys + ? ` + ${BROWSER_PASSKEYS_ENTITLEMENT} + ` + : ""; return ` @@ -1307,7 +1396,7 @@ export function renderMacPasskeyEntitlements( com.apple.developer.associated-domains ${associatedDomains} - + ${keychainAccessGroups}${browserPasskeys} com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory @@ -1535,6 +1624,7 @@ export function createStageWorkspaceConfig(input: { ? { patchedDependencies } : {}), ...(overrides && Object.keys(overrides).length > 0 ? { overrides } : {}), + packageExtensions: STAGE_PACKAGE_EXTENSIONS, }; } @@ -3673,13 +3763,24 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( const macEntitlementsPath = macPasskeySigning ? path.join(stageAppDir, "entitlements.mac.plist") : undefined; + let macWebAuthn: MacWebAuthnEntitlements | undefined; if (macPasskeySigning && macEntitlementsPath) { if (!(yield* fs.exists(macPasskeySigning.provisioningProfilePath))) { return yield* new MacProvisioningProfileNotFoundError({ provisioningProfilePath: macPasskeySigning.provisioningProfilePath, }); } - yield* fs.writeFileString(macEntitlementsPath, renderMacPasskeyEntitlements(macPasskeySigning)); + macWebAuthn = resolveMacWebAuthnEntitlements( + yield* fs.readFileString(macPasskeySigning.provisioningProfilePath), + macPasskeySigning, + ); + yield* Effect.log( + `[desktop-artifact] In-app browser passkeys: Touch ID ${macWebAuthn.touchIdKeychainAccessGroup ? "enabled" : "disabled"}, browser passkeys ${macWebAuthn.browserPasskeys ? "enabled" : "disabled"}.`, + ); + yield* fs.writeFileString( + macEntitlementsPath, + renderMacPasskeyEntitlements(macPasskeySigning, macWebAuthn), + ); } // Windows splits dependencies per process: app.asar carries only the @@ -3709,6 +3810,8 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( version: appVersion, buildVersion: appVersion, t3codeCommitHash: commitHash, + // Read by apps/desktop/src/preview/Passkeys.ts; must match the signed entitlements. + ...(macWebAuthn ? { t3codeWebAuthn: macWebAuthn } : {}), private: true, packageManager: rootPackageJson.packageManager, description: diff --git a/scripts/lib/desktop-external-packages.ts b/scripts/lib/desktop-external-packages.ts index 919397a684d1..5fb2b2df15c0 100644 --- a/scripts/lib/desktop-external-packages.ts +++ b/scripts/lib/desktop-external-packages.ts @@ -16,6 +16,7 @@ export const DESKTOP_RUNTIME_EXTERNAL_PREFIXES = [ "@napi-rs/keyring", "@crowecawcaw/xa11y", "@clerk/electron-passkeys", + "electron-webauthn", "ffi-rs", "@yuuang/", // Reads its own bundle from disk by resolving `playwright-core/package.json` diff --git a/scripts/package.json b/scripts/package.json index 30890bde1002..ed30ba91b7b8 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -13,11 +13,13 @@ "@t3tools/shared": "workspace:*", "@t3tools/tailscale": "workspace:*", "effect": "catalog:", + "plist": "catalog:", "pngjs": "7.0.0", "sharp": "0.35.4" }, "devDependencies": { "@effect/vitest": "catalog:", + "@types/plist": "catalog:", "@types/pngjs": "6.0.5", "typescript": "catalog:", "typescript-legacy": "npm:typescript@~6.0.3", diff --git a/third-party-licenses.config.json b/third-party-licenses.config.json index 3376a841e78d..9102be3af278 100644 --- a/third-party-licenses.config.json +++ b/third-party-licenses.config.json @@ -278,6 +278,27 @@ "copyrights": ["Copyright (c) 2025 Stephen Crowe"] } }, + { + "license": "MIT", + "repositoryUrl": "https://github.com/iamEvanYT/electron-webauthn", + "generatedNotice": { + "licenseId": "MIT", + "copyrights": ["Copyright (c) 2026 iamEvan"] + } + }, + { + "license": "MIT", + "name": "@electron-webauthn/types", + "sourceUrl": "https://github.com/iamEvanYT/electron-webauthn", + "generatedNotice": { + "licenseId": "MIT", + "copyrights": ["Copyright (c) 2026 iamEvan"] + } + }, + { + "license": "MIT", + "name": "objc-js" + }, { "license": "MIT", "name": "map-stream", From a1db449fe4fecafcb473340771163e791b7fb3d6 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 13:29:41 -0700 Subject: [PATCH 08/39] fix(client): load earlier turns works for MCP threads over T3 Connect (#17599) Co-authored-by: Claude Opus 5.5 --- .../src/state/boundedThreadSnapshotHttp.ts | 5 ++- .../src/state/environmentHttpAuth.test.ts | 45 +++++++++++++++++++ .../src/state/threadHistoryHttp.ts | 5 ++- .../src/state/threadSnapshotHttp.ts | 5 ++- 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts b/packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts index a049a6b14863..1a487e9b9d67 100644 --- a/packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts +++ b/packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts @@ -40,7 +40,10 @@ export const fetchEnvironmentBoundedThreadSnapshot = Effect.fn( group: "orchestration", method: "GET", url: (httpBaseUrl) => - environmentEndpointUrl(httpBaseUrl, `/api/orchestration/threads/${input.threadId}/bounded`), + environmentEndpointUrl( + httpBaseUrl, + `/api/orchestration/threads/${encodeURIComponent(input.threadId)}/bounded`, + ), timeoutMs: input.timeoutMs ?? DEFAULT_BOUNDED_THREAD_SNAPSHOT_TIMEOUT_MS, request: ({ client, headers }) => client.threadBoundedSnapshot({ diff --git a/packages/client-runtime/src/state/environmentHttpAuth.test.ts b/packages/client-runtime/src/state/environmentHttpAuth.test.ts index 61504668427e..0a2df6f936a5 100644 --- a/packages/client-runtime/src/state/environmentHttpAuth.test.ts +++ b/packages/client-runtime/src/state/environmentHttpAuth.test.ts @@ -4,6 +4,7 @@ import { ORCHESTRATION_PROTOCOL_HEADER, ORCHESTRATION_PROTOCOL_VERSION_TEXT, ProjectId, + ThreadId, type AuthSessionState, type OrchestrationV2ShellSnapshot, OrchestrationV2ThreadDetailSnapshot, @@ -307,6 +308,50 @@ describe("authenticated environment HTTP requests", () => { }), ); + // MCP-created thread ids contain ":", which the request path percent-encodes. + // The DPoP proof must sign the URL that is actually sent, or the environment + // rejects it as a URL mismatch. + const MCP_THREAD_ID = ThreadId.make("mcp:3534bc83-1c17-4a1e-9118-601c2766d355"); + const MCP_THREAD_LOADERS: ReadonlyArray< + Pick<(typeof LOADERS)[number], "name" | "response" | "load"> + > = [ + { + name: "thread snapshot", + response: encodeThreadSnapshot(THREAD), + load: (input: HttpInput) => + ThreadSnapshotLoader.fetchEnvironmentThreadSnapshot({ ...input, threadId: MCP_THREAD_ID }), + }, + { + name: "bounded thread snapshot", + response: encodeBoundedSnapshot(BOUNDED_THREAD), + load: (input: HttpInput) => + fetchEnvironmentBoundedThreadSnapshot({ ...input, threadId: MCP_THREAD_ID }), + }, + { + name: "older thread history", + response: THREAD_HISTORY, + load: (input: HttpInput) => + fetchEnvironmentThreadHistoryPage({ + ...input, + threadId: MCP_THREAD_ID, + cursor: "older-page", + }), + }, + ]; + it.effect.each(MCP_THREAD_LOADERS)( + "signs the sent URL for a $name of a thread id that needs encoding", + (loader) => + Effect.gen(function* () { + const harness = makeHarness(() => Response.json(loader.response)); + yield* loader.load(harness.input).pipe(Effect.provide(harness.httpLayer)); + + const sent = new URL(harness.calls[0]!.url); + expect(sent.pathname).toContain("/mcp%3A3534bc83-"); + sent.search = ""; + expect(harness.proofs.map((proof) => proof.url)).toEqual([sent.toString()]); + }), + ); + it.effect("retries a rejected diff once with a new token, endpoint, and proof", () => Effect.gen(function* () { const harness = makeHarness((requestNumber) => diff --git a/packages/client-runtime/src/state/threadHistoryHttp.ts b/packages/client-runtime/src/state/threadHistoryHttp.ts index b83cb6f20c35..316e0567305a 100644 --- a/packages/client-runtime/src/state/threadHistoryHttp.ts +++ b/packages/client-runtime/src/state/threadHistoryHttp.ts @@ -30,7 +30,10 @@ export const fetchEnvironmentThreadHistoryPage = Effect.fn( group: "orchestration", method: "GET", url: (httpBaseUrl) => - environmentEndpointUrl(httpBaseUrl, `/api/orchestration/threads/${input.threadId}/history`), + environmentEndpointUrl( + httpBaseUrl, + `/api/orchestration/threads/${encodeURIComponent(input.threadId)}/history`, + ), timeoutMs: input.timeoutMs ?? DEFAULT_THREAD_HISTORY_TIMEOUT_MS, request: ({ client, headers }) => client.threadHistoryPage({ diff --git a/packages/client-runtime/src/state/threadSnapshotHttp.ts b/packages/client-runtime/src/state/threadSnapshotHttp.ts index 8b31e4c60687..905d8872d589 100644 --- a/packages/client-runtime/src/state/threadSnapshotHttp.ts +++ b/packages/client-runtime/src/state/threadSnapshotHttp.ts @@ -66,7 +66,10 @@ export const fetchEnvironmentThreadSnapshot = Effect.fn( group: "orchestration", method: "GET", url: (httpBaseUrl) => - environmentEndpointUrl(httpBaseUrl, `/api/orchestration/threads/${input.threadId}`), + environmentEndpointUrl( + httpBaseUrl, + `/api/orchestration/threads/${encodeURIComponent(input.threadId)}`, + ), timeoutMs: input.timeoutMs ?? DEFAULT_THREAD_SNAPSHOT_TIMEOUT_MS, request: ({ client, headers }) => client.threadSnapshot({ From 0e7abea7c98060db537fe81f31ce5ba7c6d376d0 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 14:29:04 -0700 Subject: [PATCH 09/39] refactor(client): sign relay request URLs built from the HttpApi contract (#17602) Co-authored-by: Claude Opus 5.5 --- .../src/state/boundedThreadSnapshotHttp.ts | 14 +++++------ .../src/state/deviceHubAccess.ts | 2 +- .../src/state/environmentHttpAuth.test.ts | 16 +++++------- .../src/state/environmentHttpAuth.ts | 16 +++++++++--- .../src/state/pullRequestDiffHttp.ts | 7 ++---- packages/client-runtime/src/state/session.ts | 3 +-- .../src/state/shellSnapshotHttp.ts | 3 +-- .../src/state/threadHistoryHttp.ts | 25 ++++++++----------- .../src/state/threadSnapshotHttp.ts | 7 +----- 9 files changed, 41 insertions(+), 52 deletions(-) diff --git a/packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts b/packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts index 1a487e9b9d67..9f9b896b42c1 100644 --- a/packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts +++ b/packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts @@ -8,7 +8,6 @@ import { HttpClient } from "effect/http"; import * as RemoteEnvironmentAuthorization from "../authorization/service.ts"; import type { PreparedConnection } from "../connection/model.ts"; -import { environmentEndpointUrl } from "../environment/endpoint.ts"; import * as ManagedRelay from "../relay/managedRelay.ts"; import { executeAuthenticatedEnvironmentHttpRequest, @@ -35,20 +34,19 @@ export const fetchEnvironmentBoundedThreadSnapshot = Effect.fn( >; readonly timeoutMs?: number; }) { + const endpoint = { + params: { threadId: input.threadId }, + query: { compactTurnItems: "1" }, + }; return yield* executeAuthenticatedEnvironmentHttpRequest({ ...input, group: "orchestration", method: "GET", - url: (httpBaseUrl) => - environmentEndpointUrl( - httpBaseUrl, - `/api/orchestration/threads/${encodeURIComponent(input.threadId)}/bounded`, - ), + url: (urls) => urls.threadBoundedSnapshot(endpoint), timeoutMs: input.timeoutMs ?? DEFAULT_BOUNDED_THREAD_SNAPSHOT_TIMEOUT_MS, request: ({ client, headers }) => client.threadBoundedSnapshot({ - params: { threadId: input.threadId }, - query: { compactTurnItems: "1" }, + ...endpoint, headers: withOrchestrationProtocolHeader(headers), }), }).pipe( diff --git a/packages/client-runtime/src/state/deviceHubAccess.ts b/packages/client-runtime/src/state/deviceHubAccess.ts index 9681f27f6ab5..f585efbcb727 100644 --- a/packages/client-runtime/src/state/deviceHubAccess.ts +++ b/packages/client-runtime/src/state/deviceHubAccess.ts @@ -46,7 +46,7 @@ export const resolveDeviceHubAccess = Effect.fn("clientRuntime.state.resolveDevi remoteAuthorization, group: "auth", method: "POST", - url: (httpBaseUrl) => environmentEndpointUrl(httpBaseUrl, "/api/auth/websocket-ticket"), + url: (urls) => urls.webSocketTicket(), timeoutMs: TICKET_TIMEOUT_MS, request: ({ client, headers }) => client.webSocketTicket({ headers }), }); diff --git a/packages/client-runtime/src/state/environmentHttpAuth.test.ts b/packages/client-runtime/src/state/environmentHttpAuth.test.ts index 0a2df6f936a5..197f1962a716 100644 --- a/packages/client-runtime/src/state/environmentHttpAuth.test.ts +++ b/packages/client-runtime/src/state/environmentHttpAuth.test.ts @@ -294,12 +294,9 @@ describe("authenticated environment HTTP requests", () => { ); } expect(harness.authorizations).toEqual([{ expectedEnvironmentId: TARGET.environmentId }]); + // The proof signs exactly the URL sent; signers drop the query for `htu`. expect(harness.proofs).toEqual([ - { - method: loader.method, - url: `${CURRENT_ORIGIN}${loader.path}`, - accessToken: "current-token", - }, + { method: loader.method, url: call.url, accessToken: "current-token" }, ]); if (loader.name === "older thread history") { expect(url.searchParams.get("cursor")).toBe("older-page"); @@ -345,10 +342,9 @@ describe("authenticated environment HTTP requests", () => { const harness = makeHarness(() => Response.json(loader.response)); yield* loader.load(harness.input).pipe(Effect.provide(harness.httpLayer)); - const sent = new URL(harness.calls[0]!.url); - expect(sent.pathname).toContain("/mcp%3A3534bc83-"); - sent.search = ""; - expect(harness.proofs.map((proof) => proof.url)).toEqual([sent.toString()]); + const sent = harness.calls[0]!.url; + expect(new URL(sent).pathname).toContain("/mcp%3A3534bc83-"); + expect(harness.proofs.map((proof) => proof.url)).toEqual([sent]); }), ); @@ -408,7 +404,7 @@ describe("authenticated environment HTTP requests", () => { ); expect(harness.proofs[1]).toEqual({ method: "GET", - url: `${RENEWED_ORIGIN}${loader.path}`, + url: retried.url, accessToken: "renewed-token", }); if (loader.name === "older thread history") { diff --git a/packages/client-runtime/src/state/environmentHttpAuth.ts b/packages/client-runtime/src/state/environmentHttpAuth.ts index d8aa88327c7b..9693c7215b3c 100644 --- a/packages/client-runtime/src/state/environmentHttpAuth.ts +++ b/packages/client-runtime/src/state/environmentHttpAuth.ts @@ -14,6 +14,7 @@ import type { ManagedRelayDpopSigner } from "../relay/managedRelay.ts"; import { executeEnvironmentHttpRequest, makeEnvironmentHttpApiGroupClient, + makeEnvironmentHttpApiUrlBuilder, RemoteEnvironmentAuthFetchError, RemoteEnvironmentAuthTimeoutError, type RemoteEnvironmentRequestError, @@ -131,7 +132,12 @@ const executeEnvironmentRequest = Effect.fnUntraced(function* < readonly signer: Option.Option; readonly remoteAuthorization?: Option.Option; readonly method: HttpMethod.HttpMethod; - readonly url: (httpBaseUrl: string) => string; + /** + * The URL to sign, from the group's contract URL builder. It encodes path + * params exactly like the request client, so the DPoP proof matches the + * URL actually sent. + */ + readonly url: (urls: ReturnType[Group]) => string; readonly timeoutMs: number; readonly group: Group; readonly request: (input: { @@ -146,6 +152,8 @@ const executeEnvironmentRequest = Effect.fnUntraced(function* < Effect.Services>> | R > { let httpBaseUrl = input.prepared.httpBaseUrl; + const requestUrlFor = (baseUrl: string) => + input.url(makeEnvironmentHttpApiUrlBuilder(baseUrl)[input.group]); return yield* Effect.gen(function* () { let rejectedAccessToken: string | undefined; for (;;) { @@ -179,7 +187,7 @@ const executeEnvironmentRequest = Effect.fnUntraced(function* < authorization = current.httpAuthorization; } - const requestUrl = input.url(httpBaseUrl); + const requestUrl = requestUrlFor(httpBaseUrl); const client = yield* makeEnvironmentHttpApiGroupClient(httpBaseUrl, input.group); const headers = yield* buildEnvironmentAuthHeaders( authorization, @@ -225,7 +233,9 @@ const executeEnvironmentRequest = Effect.fnUntraced(function* < Effect.timeoutOrElse({ duration: input.timeoutMs, orElse: () => - Effect.fail(new RemoteEnvironmentAuthTimeoutError(input.url(httpBaseUrl), input.timeoutMs)), + Effect.fail( + new RemoteEnvironmentAuthTimeoutError(requestUrlFor(httpBaseUrl), input.timeoutMs), + ), }), ); }); diff --git a/packages/client-runtime/src/state/pullRequestDiffHttp.ts b/packages/client-runtime/src/state/pullRequestDiffHttp.ts index 1ca490cb21f7..89c56a95c44b 100644 --- a/packages/client-runtime/src/state/pullRequestDiffHttp.ts +++ b/packages/client-runtime/src/state/pullRequestDiffHttp.ts @@ -13,10 +13,7 @@ import { HttpClient } from "effect/http"; import * as RemoteEnvironmentAuthorization from "../authorization/service.ts"; import type { PreparedConnection } from "../connection/model.ts"; import * as ManagedRelay from "../relay/managedRelay.ts"; -import { - makeEnvironmentHttpApiUrlBuilder, - type RemoteEnvironmentRequestError, -} from "../rpc/http.ts"; +import { type RemoteEnvironmentRequestError } from "../rpc/http.ts"; import { executeAuthenticatedEnvironmentHttpRequest } from "./environmentHttpAuth.ts"; const DEFAULT_PULL_REQUEST_DIFF_TIMEOUT_MS = 60_000; @@ -54,7 +51,7 @@ export const fetchEnvironmentPullRequestDiff = Effect.fn( ...input, group: "pullRequests", method: "POST", - url: (httpBaseUrl) => makeEnvironmentHttpApiUrlBuilder(httpBaseUrl).pullRequests.diff(), + url: (urls) => urls.diff(), timeoutMs: input.timeoutMs ?? DEFAULT_PULL_REQUEST_DIFF_TIMEOUT_MS, request: ({ client, headers }) => client.diff({ payload: input.diff, headers }), }).pipe( diff --git a/packages/client-runtime/src/state/session.ts b/packages/client-runtime/src/state/session.ts index 63c23d735fa9..d185d3b7ee91 100644 --- a/packages/client-runtime/src/state/session.ts +++ b/packages/client-runtime/src/state/session.ts @@ -11,7 +11,6 @@ import * as RemoteEnvironmentAuthorization from "../authorization/service.ts"; import * as EnvironmentRegistry from "../connection/registry.ts"; import type { PreparedConnection } from "../connection/model.ts"; import * as EnvironmentSupervisor from "../connection/supervisor.ts"; -import { environmentEndpointUrl } from "../environment/endpoint.ts"; import * as ManagedRelay from "../relay/managedRelay.ts"; import { safeErrorLogAttributes } from "../errors/safeLog.ts"; import { executeAuthenticatedEnvironmentHttpRequest } from "./environmentHttpAuth.ts"; @@ -59,7 +58,7 @@ export const fetchEnvironmentSessionState = Effect.fn( ...input, group: "auth", method: "GET", - url: (httpBaseUrl) => environmentEndpointUrl(httpBaseUrl, "/api/auth/session"), + url: (urls) => urls.session(), timeoutMs: input.timeoutMs ?? DEFAULT_SESSION_STATE_TIMEOUT_MS, request: ({ client, headers }) => client.session({ headers }), // This endpoint returns 200 with authenticated:false for expired credentials. diff --git a/packages/client-runtime/src/state/shellSnapshotHttp.ts b/packages/client-runtime/src/state/shellSnapshotHttp.ts index 14e57be1fa67..e0294ba0dfe7 100644 --- a/packages/client-runtime/src/state/shellSnapshotHttp.ts +++ b/packages/client-runtime/src/state/shellSnapshotHttp.ts @@ -9,7 +9,6 @@ import { HttpClient, HttpClientError, type HttpClientResponse } from "effect/htt import * as RemoteEnvironmentAuthorization from "../authorization/service.ts"; import type { PreparedConnection } from "../connection/model.ts"; -import { environmentEndpointUrl } from "../environment/endpoint.ts"; import * as ManagedRelay from "../relay/managedRelay.ts"; import { executeAuthenticatedEnvironmentHttpRequest, @@ -48,7 +47,7 @@ export const fetchEnvironmentShellSnapshot = Effect.fn( ...input, group: "orchestration", method: "GET", - url: (httpBaseUrl) => environmentEndpointUrl(httpBaseUrl, "/api/orchestration/shell"), + url: (urls) => urls.shellSnapshot(), timeoutMs: input.timeoutMs ?? DEFAULT_SHELL_SNAPSHOT_TIMEOUT_MS, // The body is decoded here so each thread's pull request links can decode after its // rows. Every declared error is an `EnvironmentHttpCommonError`, so decoding one keeps diff --git a/packages/client-runtime/src/state/threadHistoryHttp.ts b/packages/client-runtime/src/state/threadHistoryHttp.ts index 316e0567305a..2d99d7dbf0e0 100644 --- a/packages/client-runtime/src/state/threadHistoryHttp.ts +++ b/packages/client-runtime/src/state/threadHistoryHttp.ts @@ -4,7 +4,6 @@ import * as Option from "effect/Option"; import type { RemoteEnvironmentAuthorization } from "../authorization/service.ts"; import type { PreparedConnection } from "../connection/model.ts"; -import { environmentEndpointUrl } from "../environment/endpoint.ts"; import * as ManagedRelay from "../relay/managedRelay.ts"; import { executeAuthenticatedEnvironmentHttpRequest, @@ -25,25 +24,21 @@ export const fetchEnvironmentThreadHistoryPage = Effect.fn( readonly remoteAuthorization?: Option.Option; readonly timeoutMs?: number; }) { + const endpoint = { + params: { threadId: input.threadId }, + query: { + cursor: input.cursor, + ...(input.view === undefined ? {} : { view: input.view }), + ...(input.throughEntryId === undefined ? {} : { throughEntryId: input.throughEntryId }), + }, + }; return yield* executeAuthenticatedEnvironmentHttpRequest({ ...input, group: "orchestration", method: "GET", - url: (httpBaseUrl) => - environmentEndpointUrl( - httpBaseUrl, - `/api/orchestration/threads/${encodeURIComponent(input.threadId)}/history`, - ), + url: (urls) => urls.threadHistoryPage(endpoint), timeoutMs: input.timeoutMs ?? DEFAULT_THREAD_HISTORY_TIMEOUT_MS, request: ({ client, headers }) => - client.threadHistoryPage({ - params: { threadId: input.threadId }, - query: { - cursor: input.cursor, - ...(input.view === undefined ? {} : { view: input.view }), - ...(input.throughEntryId === undefined ? {} : { throughEntryId: input.throughEntryId }), - }, - headers: withOrchestrationProtocolHeader(headers), - }), + client.threadHistoryPage({ ...endpoint, headers: withOrchestrationProtocolHeader(headers) }), }); }); diff --git a/packages/client-runtime/src/state/threadSnapshotHttp.ts b/packages/client-runtime/src/state/threadSnapshotHttp.ts index 905d8872d589..4987a86bf0a6 100644 --- a/packages/client-runtime/src/state/threadSnapshotHttp.ts +++ b/packages/client-runtime/src/state/threadSnapshotHttp.ts @@ -5,7 +5,6 @@ import * as Option from "effect/Option"; import * as RemoteEnvironmentAuthorization from "../authorization/service.ts"; import type { PreparedConnection } from "../connection/model.ts"; -import { environmentEndpointUrl } from "../environment/endpoint.ts"; import * as ManagedRelay from "../relay/managedRelay.ts"; import type { RemoteEnvironmentRequestError } from "../rpc/http.ts"; import { @@ -65,11 +64,7 @@ export const fetchEnvironmentThreadSnapshot = Effect.fn( ...input, group: "orchestration", method: "GET", - url: (httpBaseUrl) => - environmentEndpointUrl( - httpBaseUrl, - `/api/orchestration/threads/${encodeURIComponent(input.threadId)}`, - ), + url: (urls) => urls.threadSnapshot({ params: { threadId: input.threadId } }), timeoutMs: input.timeoutMs ?? DEFAULT_THREAD_SNAPSHOT_TIMEOUT_MS, request: ({ client, headers }) => client.threadSnapshot({ From 28f11ed7a5a2251ebe4689643b64425760ee5fe4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 14:49:59 -0700 Subject: [PATCH 10/39] refactor(source-control): add @t3tools/source-control-core (#17573) Co-authored-by: Claude Opus 5.5 --- apps/server/package.json | 1 + apps/server/scripts/evaluate-thread-titles.ts | 2 + apps/server/scripts/measure-pr-preview.ts | 2 +- apps/server/src/git/GitManager.test.ts | 2 +- .../PullRequestSyncReactor.test.ts | 2 +- .../PullRequestSyncReactor.ts | 2 +- .../PullRequestWatchReactor.ts | 2 +- .../src/orchestration-v2/runtimeLayer.test.ts | 2 +- .../pullRequest/AzureDevOpsPullRequestCli.ts | 2 +- .../AzureDevOpsPullRequestProvider.ts | 2 +- .../pullRequest/BitbucketPullRequestApi.ts | 2 +- .../BitbucketPullRequestProvider.ts | 2 +- .../pullRequest/ForgejoPullRequestProvider.ts | 2 +- .../pullRequest/GitHubPullRequestApi.test.ts | 2 +- .../src/pullRequest/GitHubPullRequestApi.ts | 9 ++- .../pullRequest/GitHubPullRequestProvider.ts | 2 +- .../src/pullRequest/GitLabPullRequestCli.ts | 2 +- .../pullRequest/GitLabPullRequestProvider.ts | 2 +- .../PullRequestProviderRateLimit.test.ts | 2 +- .../PullRequestProviderRegistry.ts | 2 +- .../pullRequest/PullRequestService.test.ts | 4 +- .../src/pullRequest/PullRequestService.ts | 4 +- .../pullRequest/bitbucketPullRequestJson.ts | 2 +- .../src/pullRequest/forgejoPullRequestJson.ts | 4 +- .../pullRequest/gitHubConditionalChecks.ts | 2 +- .../src/pullRequest/gitHubPullRequestJson.ts | 2 +- .../src/pullRequest/pullRequestViewedFiles.ts | 5 +- apps/server/src/server.ts | 4 +- .../src/sourceControl/AzureDevOpsCli.ts | 2 +- .../AzureDevOpsSourceControlProvider.ts | 4 +- apps/server/src/sourceControl/BitbucketApi.ts | 4 +- .../BitbucketSourceControlProvider.ts | 4 +- apps/server/src/sourceControl/ForgejoCli.ts | 2 +- .../ForgejoSourceControlProvider.ts | 4 +- .../src/sourceControl/GitHubApi.test.ts | 2 +- apps/server/src/sourceControl/GitHubApi.ts | 2 +- .../GitHubSourceControlProvider.ts | 6 +- apps/server/src/sourceControl/GitLabCli.ts | 2 +- .../GitLabSourceControlProvider.ts | 4 +- .../sourceControl/ServerSourceControlHost.ts | 24 ++++++++ .../SourceControlDiscovery.test.ts | 11 +++- .../sourceControl/SourceControlDiscovery.ts | 2 +- .../SourceControlProviderRegistry.test.ts | 6 ++ .../SourceControlProviderRegistry.ts | 9 +-- .../SourceControlRepositoryService.test.ts | 2 +- .../src/sourceControl/githubQuota.test.ts | 2 +- apps/server/src/sourceControl/githubQuota.ts | 2 +- apps/server/src/ws.ts | 2 + docs/internals/pull-request-file-revisions.md | 2 +- package.json | 2 +- packages/source-control-core/package.json | 48 +++++++++++++++ .../src/server}/PullRequestProvider.ts | 0 .../src/server/SourceControlHost.ts | 59 +++++++++++++++++++ .../src/server}/SourceControlProvider.test.ts | 0 .../src/server}/SourceControlProvider.ts | 2 +- .../server}/SourceControlRateLimit.test.ts | 0 .../src/server}/SourceControlRateLimit.ts | 11 ++-- .../src/server/discovery.ts | 15 +++-- .../src/server}/mergeMessage.test.ts | 0 .../src/server}/mergeMessage.ts | 0 .../src/server}/pullRequestChecks.test.ts | 0 .../src/server}/pullRequestChecks.ts | 0 packages/source-control-core/tsconfig.json | 7 +++ pnpm-lock.yaml | 22 +++++++ scripts/release-smoke.ts | 1 + 65 files changed, 268 insertions(+), 70 deletions(-) create mode 100644 apps/server/src/sourceControl/ServerSourceControlHost.ts create mode 100644 packages/source-control-core/package.json rename {apps/server/src/pullRequest => packages/source-control-core/src/server}/PullRequestProvider.ts (100%) create mode 100644 packages/source-control-core/src/server/SourceControlHost.ts rename {apps/server/src/sourceControl => packages/source-control-core/src/server}/SourceControlProvider.test.ts (100%) rename {apps/server/src/sourceControl => packages/source-control-core/src/server}/SourceControlProvider.ts (98%) rename {apps/server/src/sourceControl => packages/source-control-core/src/server}/SourceControlRateLimit.test.ts (100%) rename {apps/server/src/sourceControl => packages/source-control-core/src/server}/SourceControlRateLimit.ts (95%) rename apps/server/src/sourceControl/SourceControlProviderDiscovery.ts => packages/source-control-core/src/server/discovery.ts (95%) rename {apps/server/src/pullRequest => packages/source-control-core/src/server}/mergeMessage.test.ts (100%) rename {apps/server/src/pullRequest => packages/source-control-core/src/server}/mergeMessage.ts (100%) rename {apps/server/src/pullRequest => packages/source-control-core/src/server}/pullRequestChecks.test.ts (100%) rename {apps/server/src/pullRequest => packages/source-control-core/src/server}/pullRequestChecks.ts (100%) create mode 100644 packages/source-control-core/tsconfig.json diff --git a/apps/server/package.json b/apps/server/package.json index bb166039abd9..ea7b9019e415 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -45,6 +45,7 @@ "@t3tools/provider-muse": "workspace:*", "@t3tools/provider-opencode": "workspace:*", "@t3tools/provider-pi": "workspace:*", + "@t3tools/source-control-core": "workspace:*", "diff": "8.0.3", "effect": "catalog:", "jose": "catalog:", diff --git a/apps/server/scripts/evaluate-thread-titles.ts b/apps/server/scripts/evaluate-thread-titles.ts index 0747825dcb5b..196bcbdfbcaa 100644 --- a/apps/server/scripts/evaluate-thread-titles.ts +++ b/apps/server/scripts/evaluate-thread-titles.ts @@ -23,6 +23,7 @@ import { } from "@t3tools/provider-core/server/threadTitleContext"; import * as ThreadTitleLinks from "../src/textGeneration/ThreadTitleLinks.ts"; import * as SourceControlProviderRegistry from "../src/sourceControl/SourceControlProviderRegistry.ts"; +import * as ServerSourceControlHost from "../src/sourceControl/ServerSourceControlHost.ts"; import * as GitHubApi from "../src/sourceControl/GitHubApi.ts"; import * as GitLabCli from "../src/sourceControl/GitLabCli.ts"; import * as ForgejoCli from "../src/sourceControl/ForgejoCli.ts"; @@ -158,6 +159,7 @@ await Effect.runPromise( AzureDevOpsCli.layer, // No saved credentials here; Bitbucket falls back to T3CODE_BITBUCKET_* variables. BitbucketApi.layer, + ServerSourceControlHost.layer, ), ), // Default settings: no saved Bitbucket token, gh's own GitHub account choice. diff --git a/apps/server/scripts/measure-pr-preview.ts b/apps/server/scripts/measure-pr-preview.ts index e8f0727ac93c..0f33ec9a6139 100644 --- a/apps/server/scripts/measure-pr-preview.ts +++ b/apps/server/scripts/measure-pr-preview.ts @@ -16,7 +16,7 @@ import * as GitHubApi from "../src/sourceControl/GitHubApi.ts"; import * as GitHubCredentials from "../src/sourceControl/GitHubCredentials.ts"; import * as ServerSettings from "../src/serverSettings.ts"; import * as GitHubQuota from "../src/sourceControl/githubQuota.ts"; -import * as SourceControlRateLimit from "../src/sourceControl/SourceControlRateLimit.ts"; +import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; import * as VcsProcess from "../src/vcs/VcsProcess.ts"; const [repository, ...numbers] = process.argv.slice(2); diff --git a/apps/server/src/git/GitManager.test.ts b/apps/server/src/git/GitManager.test.ts index 9f5f368d2cf2..c7e76f5e71ab 100644 --- a/apps/server/src/git/GitManager.test.ts +++ b/apps/server/src/git/GitManager.test.ts @@ -58,7 +58,7 @@ import { ForgejoPullRequestSchema, toForgejoChangeRequest, } from "../sourceControl/forgejoPullRequests.ts"; -import type { SourceControlProvider } from "../sourceControl/SourceControlProvider.ts"; +import type { SourceControlProvider } from "@t3tools/source-control-core/server/SourceControlProvider"; import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts"; import * as ServerConfig from "../config.ts"; import * as ProjectionStore from "../orchestration-v2/ProjectionStore.ts"; diff --git a/apps/server/src/orchestration-v2/PullRequestSyncReactor.test.ts b/apps/server/src/orchestration-v2/PullRequestSyncReactor.test.ts index 36f4bdb15891..19e67f949b61 100644 --- a/apps/server/src/orchestration-v2/PullRequestSyncReactor.test.ts +++ b/apps/server/src/orchestration-v2/PullRequestSyncReactor.test.ts @@ -31,7 +31,7 @@ import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; import { TestClock } from "effect/testing"; -import { PullRequestProviderError } from "../pullRequest/PullRequestProvider.ts"; +import { PullRequestProviderError } from "@t3tools/source-control-core/server/PullRequestProvider"; import * as GitManager from "../git/GitManager.ts"; import * as PullRequestService from "../pullRequest/PullRequestService.ts"; import * as ServerActivation from "../serverActivation.ts"; diff --git a/apps/server/src/orchestration-v2/PullRequestSyncReactor.ts b/apps/server/src/orchestration-v2/PullRequestSyncReactor.ts index 5f1fcd8269ac..a966cbd95456 100644 --- a/apps/server/src/orchestration-v2/PullRequestSyncReactor.ts +++ b/apps/server/src/orchestration-v2/PullRequestSyncReactor.ts @@ -29,7 +29,7 @@ import * as Semaphore from "effect/Semaphore"; import * as Stream from "effect/Stream"; import * as GitManager from "../git/GitManager.ts"; -import { PullRequestProviderError } from "../pullRequest/PullRequestProvider.ts"; +import { PullRequestProviderError } from "@t3tools/source-control-core/server/PullRequestProvider"; import * as PullRequestService from "../pullRequest/PullRequestService.ts"; import { forkParked } from "../serverActivation.ts"; import * as Orchestrator from "./Orchestrator.ts"; diff --git a/apps/server/src/orchestration-v2/PullRequestWatchReactor.ts b/apps/server/src/orchestration-v2/PullRequestWatchReactor.ts index 7005534f89ed..5a5f61ae3c4d 100644 --- a/apps/server/src/orchestration-v2/PullRequestWatchReactor.ts +++ b/apps/server/src/orchestration-v2/PullRequestWatchReactor.ts @@ -29,7 +29,7 @@ import type * as Scope from "effect/Scope"; import { type ProviderChangeRequestWatchFingerprint, PullRequestProviderError, -} from "../pullRequest/PullRequestProvider.ts"; +} from "@t3tools/source-control-core/server/PullRequestProvider"; import * as PullRequestService from "../pullRequest/PullRequestService.ts"; import { forkParked } from "../serverActivation.ts"; import * as Orchestrator from "./Orchestrator.ts"; diff --git a/apps/server/src/orchestration-v2/runtimeLayer.test.ts b/apps/server/src/orchestration-v2/runtimeLayer.test.ts index b11184f44632..bb70c880b061 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.test.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts @@ -65,7 +65,7 @@ import * as ProviderRuntimeRecoveryService from "./ProviderRuntimeRecoveryServic import * as ProjectionMaintenance from "./ProjectionMaintenance.ts"; import * as ProjectionStore from "./ProjectionStore.ts"; import * as PullRequestWatchReactor from "./PullRequestWatchReactor.ts"; -import { PullRequestProviderError } from "../pullRequest/PullRequestProvider.ts"; +import { PullRequestProviderError } from "@t3tools/source-control-core/server/PullRequestProvider"; import * as PullRequestService from "../pullRequest/PullRequestService.ts"; import * as ProjectStore from "./ProjectStore.ts"; import * as ProviderSessionManager from "./ProviderSessionManager.ts"; diff --git a/apps/server/src/pullRequest/AzureDevOpsPullRequestCli.ts b/apps/server/src/pullRequest/AzureDevOpsPullRequestCli.ts index 5f5453a9b3ba..d00d08b7d05f 100644 --- a/apps/server/src/pullRequest/AzureDevOpsPullRequestCli.ts +++ b/apps/server/src/pullRequest/AzureDevOpsPullRequestCli.ts @@ -26,7 +26,7 @@ import { type AzureDevOpsPullRequest, type AzureDevOpsRepositoryLocation, } from "./azureDevOpsPullRequestJson.ts"; -import type { ProviderListCursor } from "./PullRequestProvider.ts"; +import type { ProviderListCursor } from "@t3tools/source-control-core/server/PullRequestProvider"; /** * Names the read that produced unusable output, so a failure reports the call it came from diff --git a/apps/server/src/pullRequest/AzureDevOpsPullRequestProvider.ts b/apps/server/src/pullRequest/AzureDevOpsPullRequestProvider.ts index 2cdf78c56a14..afd045636947 100644 --- a/apps/server/src/pullRequest/AzureDevOpsPullRequestProvider.ts +++ b/apps/server/src/pullRequest/AzureDevOpsPullRequestProvider.ts @@ -24,7 +24,7 @@ import { type ProviderChangeRequestSummary, type ProviderDiffSlice, type PullRequestProviderApi, -} from "./PullRequestProvider.ts"; +} from "@t3tools/source-control-core/server/PullRequestProvider"; import type { AzureDevOpsChangeEntry, AzureDevOpsItemContent, diff --git a/apps/server/src/pullRequest/BitbucketPullRequestApi.ts b/apps/server/src/pullRequest/BitbucketPullRequestApi.ts index c7d017071fc6..f5e108b05d45 100644 --- a/apps/server/src/pullRequest/BitbucketPullRequestApi.ts +++ b/apps/server/src/pullRequest/BitbucketPullRequestApi.ts @@ -39,7 +39,7 @@ import { type BitbucketPullRequest, type BitbucketRawComment, } from "./bitbucketPullRequestJson.ts"; -import type { ProviderListCursor } from "./PullRequestProvider.ts"; +import type { ProviderListCursor } from "@t3tools/source-control-core/server/PullRequestProvider"; /** * Names the read that produced unusable output, so a failure reports the call it came from diff --git a/apps/server/src/pullRequest/BitbucketPullRequestProvider.ts b/apps/server/src/pullRequest/BitbucketPullRequestProvider.ts index f553dfd13bc1..0abcb7d95e7e 100644 --- a/apps/server/src/pullRequest/BitbucketPullRequestProvider.ts +++ b/apps/server/src/pullRequest/BitbucketPullRequestProvider.ts @@ -9,7 +9,7 @@ import { type ProviderChangeRequestActivity, type ProviderChangeRequestDetail, type PullRequestProviderApi, -} from "./PullRequestProvider.ts"; +} from "@t3tools/source-control-core/server/PullRequestProvider"; import type { BitbucketPullRequest } from "./bitbucketPullRequestJson.ts"; const CAPABILITIES: PullRequestCapabilities = { diff --git a/apps/server/src/pullRequest/ForgejoPullRequestProvider.ts b/apps/server/src/pullRequest/ForgejoPullRequestProvider.ts index 12d931279a74..a9337fdbe8fd 100644 --- a/apps/server/src/pullRequest/ForgejoPullRequestProvider.ts +++ b/apps/server/src/pullRequest/ForgejoPullRequestProvider.ts @@ -10,7 +10,7 @@ import { type ProviderChangeRequestDetail, type ProviderRepositoryRef, type PullRequestProviderApi, -} from "./PullRequestProvider.ts"; +} from "@t3tools/source-control-core/server/PullRequestProvider"; import { ForgejoPullRequest, ForgejoRepository, diff --git a/apps/server/src/pullRequest/GitHubPullRequestApi.test.ts b/apps/server/src/pullRequest/GitHubPullRequestApi.test.ts index 294d053d6f05..8888f4c11f45 100644 --- a/apps/server/src/pullRequest/GitHubPullRequestApi.test.ts +++ b/apps/server/src/pullRequest/GitHubPullRequestApi.test.ts @@ -16,7 +16,7 @@ import { AllowGitHubReserve } from "../sourceControl/GitHubApi.ts"; import * as GitHubApi from "../sourceControl/GitHubApi.ts"; import * as GitHubCredentials from "../sourceControl/GitHubCredentials.ts"; import * as GitHubQuota from "../sourceControl/githubQuota.ts"; -import * as SourceControlRateLimit from "../sourceControl/SourceControlRateLimit.ts"; +import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; import * as VcsProcess from "../vcs/VcsProcess.ts"; import * as GitHubPullRequestApi from "./GitHubPullRequestApi.ts"; import { KnownWorkflowRuns } from "./gitHubConditionalChecks.ts"; diff --git a/apps/server/src/pullRequest/GitHubPullRequestApi.ts b/apps/server/src/pullRequest/GitHubPullRequestApi.ts index 5e3572915460..dd093df00b77 100644 --- a/apps/server/src/pullRequest/GitHubPullRequestApi.ts +++ b/apps/server/src/pullRequest/GitHubPullRequestApi.ts @@ -1,4 +1,4 @@ -import { removeAgentCredits } from "./mergeMessage.ts"; +import { removeAgentCredits } from "@t3tools/source-control-core/server/mergeMessage"; import { KnownWorkflowRuns, makeChecksRevalidator } from "./gitHubConditionalChecks.ts"; import { runGitHubStackAction, type GitHubStackActionError } from "./githubStackActions.ts"; import * as Cause from "effect/Cause"; @@ -45,7 +45,7 @@ import { import * as GitHubApi from "../sourceControl/GitHubApi.ts"; import { readGraphQlPages } from "../sourceControl/githubGraphQl.ts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; -import * as SourceControlRateLimit from "../sourceControl/SourceControlRateLimit.ts"; +import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; import { ACTOR_AVATARS_GRAPHQL_QUERY, ADD_REACTION_GRAPHQL_MUTATION, @@ -128,7 +128,10 @@ import { type GitHubReviewThreadPage, type GitHubViewerAccess, } from "./gitHubPullRequestJson.ts"; -import type { ProviderChangeRequestSummary, ProviderListCursor } from "./PullRequestProvider.ts"; +import type { + ProviderChangeRequestSummary, + ProviderListCursor, +} from "@t3tools/source-control-core/server/PullRequestProvider"; /** * Names the read that produced unusable output, so a failure reports the call it came from diff --git a/apps/server/src/pullRequest/GitHubPullRequestProvider.ts b/apps/server/src/pullRequest/GitHubPullRequestProvider.ts index f2672dbe991f..67f871501ddb 100644 --- a/apps/server/src/pullRequest/GitHubPullRequestProvider.ts +++ b/apps/server/src/pullRequest/GitHubPullRequestProvider.ts @@ -16,7 +16,7 @@ import { type ProviderChangeRequestDetail, type PullRequestProviderApi, type ProviderRepositoryRef, -} from "./PullRequestProvider.ts"; +} from "@t3tools/source-control-core/server/PullRequestProvider"; import type { GitHubViewerAccess, GitHubWorkflowRunApproval } from "./gitHubPullRequestJson.ts"; const CAPABILITIES: PullRequestCapabilities = { diff --git a/apps/server/src/pullRequest/GitLabPullRequestCli.ts b/apps/server/src/pullRequest/GitLabPullRequestCli.ts index c3b607002497..933e68742187 100644 --- a/apps/server/src/pullRequest/GitLabPullRequestCli.ts +++ b/apps/server/src/pullRequest/GitLabPullRequestCli.ts @@ -44,7 +44,7 @@ import { type GitLabMergeRequestListItem, type GitLabProjectUsers, } from "./gitLabMergeRequestJson.ts"; -import type { ProviderListCursor } from "./PullRequestProvider.ts"; +import type { ProviderListCursor } from "@t3tools/source-control-core/server/PullRequestProvider"; /** * Names the read that produced unusable output, so a failure reports the call it came from diff --git a/apps/server/src/pullRequest/GitLabPullRequestProvider.ts b/apps/server/src/pullRequest/GitLabPullRequestProvider.ts index 45034b84787c..1b5b987cd5ac 100644 --- a/apps/server/src/pullRequest/GitLabPullRequestProvider.ts +++ b/apps/server/src/pullRequest/GitLabPullRequestProvider.ts @@ -12,7 +12,7 @@ import { type ProviderChangeRequestActivity, type ProviderChangeRequestDetail, type PullRequestProviderApi, -} from "./PullRequestProvider.ts"; +} from "@t3tools/source-control-core/server/PullRequestProvider"; const CAPABILITIES: PullRequestCapabilities = { diff: true, diff --git a/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts b/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts index 873cc32a2c7f..ca12da2b8979 100644 --- a/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts +++ b/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts @@ -3,7 +3,7 @@ import { assert, it } from "@effect/vitest"; import * as AzureDevOpsCli from "../sourceControl/AzureDevOpsCli.ts"; import * as BitbucketApi from "../sourceControl/BitbucketApi.ts"; import * as GitHubApi from "../sourceControl/GitHubApi.ts"; -import * as SourceControlRateLimit from "../sourceControl/SourceControlRateLimit.ts"; +import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; import * as GitLabCli from "../sourceControl/GitLabCli.ts"; import { azureDevOpsProviderFailure } from "./AzureDevOpsPullRequestProvider.ts"; import { bitbucketProviderFailure } from "./BitbucketPullRequestProvider.ts"; diff --git a/apps/server/src/pullRequest/PullRequestProviderRegistry.ts b/apps/server/src/pullRequest/PullRequestProviderRegistry.ts index 316cd1e7ae92..8e5a1d9cfd0b 100644 --- a/apps/server/src/pullRequest/PullRequestProviderRegistry.ts +++ b/apps/server/src/pullRequest/PullRequestProviderRegistry.ts @@ -17,7 +17,7 @@ import * as GitHubPullRequestApi from "./GitHubPullRequestApi.ts"; import * as GitHubPullRequestProvider from "./GitHubPullRequestProvider.ts"; import * as GitLabPullRequestCli from "./GitLabPullRequestCli.ts"; import * as GitLabPullRequestProvider from "./GitLabPullRequestProvider.ts"; -import type { PullRequestProviderApi } from "./PullRequestProvider.ts"; +import type { PullRequestProviderApi } from "@t3tools/source-control-core/server/PullRequestProvider"; export class PullRequestProviderRegistry extends Context.Service< PullRequestProviderRegistry, diff --git a/apps/server/src/pullRequest/PullRequestService.test.ts b/apps/server/src/pullRequest/PullRequestService.test.ts index f9015ecbd9ef..ef84de264ec6 100644 --- a/apps/server/src/pullRequest/PullRequestService.test.ts +++ b/apps/server/src/pullRequest/PullRequestService.test.ts @@ -29,14 +29,14 @@ import * as SqlitePersistence from "../persistence/Sqlite.ts"; import * as PullRequestFilesViewed from "../persistence/PullRequestFilesViewed.ts"; import * as RepositoryIdentityResolver from "../project/RepositoryIdentityResolver.ts"; import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts"; -import * as SourceControlRateLimit from "../sourceControl/SourceControlRateLimit.ts"; +import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; import * as ForgejoCli from "../sourceControl/ForgejoCli.ts"; import * as ForgejoPullRequestProvider from "./ForgejoPullRequestProvider.ts"; import { PullRequestProviderError, type ProviderChangeRequest, type PullRequestProviderApi, -} from "./PullRequestProvider.ts"; +} from "@t3tools/source-control-core/server/PullRequestProvider"; import * as PullRequestProviderRegistry from "./PullRequestProviderRegistry.ts"; import * as PullRequestService from "./PullRequestService.ts"; import * as PullRequestReadCache from "./PullRequestReadCache.ts"; diff --git a/apps/server/src/pullRequest/PullRequestService.ts b/apps/server/src/pullRequest/PullRequestService.ts index a44bd322adcb..fb14ef896cd5 100644 --- a/apps/server/src/pullRequest/PullRequestService.ts +++ b/apps/server/src/pullRequest/PullRequestService.ts @@ -83,14 +83,14 @@ import * as ServerSettings from "../serverSettings.ts"; import * as PullRequestFilesViewed from "../persistence/PullRequestFilesViewed.ts"; import * as RepositoryIdentityResolver from "../project/RepositoryIdentityResolver.ts"; import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts"; -import * as SourceControlRateLimit from "../sourceControl/SourceControlRateLimit.ts"; +import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; import { type ProviderChangeRequest, type ProviderListCursor, type ProviderChangeRequestWatchFingerprint, type PullRequestProviderApi, PullRequestProviderError, -} from "./PullRequestProvider.ts"; +} from "@t3tools/source-control-core/server/PullRequestProvider"; import * as PullRequestReadCache from "./PullRequestReadCache.ts"; import * as PullRequestProviderRegistry from "./PullRequestProviderRegistry.ts"; import * as ViewedFiles from "./pullRequestViewedFiles.ts"; diff --git a/apps/server/src/pullRequest/bitbucketPullRequestJson.ts b/apps/server/src/pullRequest/bitbucketPullRequestJson.ts index f0bc8a71a827..8f0b520ca089 100644 --- a/apps/server/src/pullRequest/bitbucketPullRequestJson.ts +++ b/apps/server/src/pullRequest/bitbucketPullRequestJson.ts @@ -18,7 +18,7 @@ import type { import { TrimmedNonEmptyString } from "@t3tools/contracts"; import { decodeJsonResult } from "@t3tools/shared/schemaJson"; -import { dedupeChecks } from "./pullRequestChecks.ts"; +import { dedupeChecks } from "@t3tools/source-control-core/server/pullRequestChecks"; /** * Bitbucket's enums are decoded as plain strings and normalized here, in the same tolerant diff --git a/apps/server/src/pullRequest/forgejoPullRequestJson.ts b/apps/server/src/pullRequest/forgejoPullRequestJson.ts index cade3b65d9c8..a937c00c2ef8 100644 --- a/apps/server/src/pullRequest/forgejoPullRequestJson.ts +++ b/apps/server/src/pullRequest/forgejoPullRequestJson.ts @@ -10,8 +10,8 @@ import type { PullRequestReactionContent, PullRequestReviewThread, } from "@t3tools/contracts"; -import type { ProviderChangeRequest } from "./PullRequestProvider.ts"; -import { dedupeChecks } from "./pullRequestChecks.ts"; +import type { ProviderChangeRequest } from "@t3tools/source-control-core/server/PullRequestProvider"; +import { dedupeChecks } from "@t3tools/source-control-core/server/pullRequestChecks"; export const ForgejoUser = Schema.Struct({ login: Schema.String, diff --git a/apps/server/src/pullRequest/gitHubConditionalChecks.ts b/apps/server/src/pullRequest/gitHubConditionalChecks.ts index 6975da251936..353089ebf0f9 100644 --- a/apps/server/src/pullRequest/gitHubConditionalChecks.ts +++ b/apps/server/src/pullRequest/gitHubConditionalChecks.ts @@ -10,7 +10,7 @@ import { PositiveInt, type PullRequestChecks } from "@t3tools/contracts"; import * as GitHubApi from "../sourceControl/GitHubApi.ts"; import type { GitHubPullRequestDetail } from "./gitHubPullRequestJson.ts"; import type { GitHubPullRequestApiError } from "./GitHubPullRequestApi.ts"; -import type { ProviderRepositoryRef } from "./PullRequestProvider.ts"; +import type { ProviderRepositoryRef } from "@t3tools/source-control-core/server/PullRequestProvider"; const HeadSchema = Schema.Struct({ head: Schema.Struct({ diff --git a/apps/server/src/pullRequest/gitHubPullRequestJson.ts b/apps/server/src/pullRequest/gitHubPullRequestJson.ts index 028231c513f2..edca11f5a0d0 100644 --- a/apps/server/src/pullRequest/gitHubPullRequestJson.ts +++ b/apps/server/src/pullRequest/gitHubPullRequestJson.ts @@ -36,7 +36,7 @@ import { quoteGitPatchPath } from "@t3tools/shared/gitPatchPath"; import { decodeJsonResult } from "@t3tools/shared/schemaJson"; import { aliasedGraphQlDocument, type GraphQlDocument } from "../sourceControl/githubGraphQl.ts"; -import { dedupeChecks } from "./pullRequestChecks.ts"; +import { dedupeChecks } from "@t3tools/source-control-core/server/pullRequestChecks"; /** * Enum-ish GitHub CLI fields are decoded as plain strings and normalized here: a `gh` diff --git a/apps/server/src/pullRequest/pullRequestViewedFiles.ts b/apps/server/src/pullRequest/pullRequestViewedFiles.ts index 1f9aa33ac796..9f64c8352af5 100644 --- a/apps/server/src/pullRequest/pullRequestViewedFiles.ts +++ b/apps/server/src/pullRequest/pullRequestViewedFiles.ts @@ -16,7 +16,10 @@ import { } from "@t3tools/contracts"; import type * as PullRequestFilesViewed from "../persistence/PullRequestFilesViewed.ts"; -import type { ProviderFileRevisions, PullRequestProviderError } from "./PullRequestProvider.ts"; +import type { + ProviderFileRevisions, + PullRequestProviderError, +} from "@t3tools/source-control-core/server/PullRequestProvider"; import type { PullRequestError, SupportedProject } from "./PullRequestService.ts"; /** diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 1bbe647dcd0c..913f8067d250 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -104,8 +104,9 @@ import * as ProjectCloneTracker from "./project/ProjectCloneTracker.ts"; import * as GitWorkflowService from "./git/GitWorkflowService.ts"; import * as ReviewService from "./review/ReviewService.ts"; import * as SourceControlProviderRegistry from "./sourceControl/SourceControlProviderRegistry.ts"; +import * as ServerSourceControlHost from "./sourceControl/ServerSourceControlHost.ts"; import * as PullRequestReadCache from "./pullRequest/PullRequestReadCache.ts"; -import * as SourceControlRateLimit from "./sourceControl/SourceControlRateLimit.ts"; +import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts"; import * as WorktreeSetupTracker from "./project/WorktreeSetupTracker.ts"; import * as Observability from "./observability/Observability.ts"; @@ -305,6 +306,7 @@ const layerSourceControlProviderRegistry = SourceControlProviderRegistry.layer.p GitHubApi.layerWithDependencies, GitLabCli.layer, ForgejoCli.layer, + ServerSourceControlHost.layer, ), ), Layer.provideMerge(GitVcsDriver.layer), diff --git a/apps/server/src/sourceControl/AzureDevOpsCli.ts b/apps/server/src/sourceControl/AzureDevOpsCli.ts index f5e436ef6c56..7fd44de3c40b 100644 --- a/apps/server/src/sourceControl/AzureDevOpsCli.ts +++ b/apps/server/src/sourceControl/AzureDevOpsCli.ts @@ -17,7 +17,7 @@ import { decodeAzureDevOpsPullRequestListJson, type NormalizedAzureDevOpsPullRequestRecord, } from "./azureDevOpsPullRequests.ts"; -import * as SourceControlProvider from "./SourceControlProvider.ts"; +import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; const DEFAULT_TIMEOUT_MS = 30_000; diff --git a/apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts b/apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts index c2f6a5efc65a..dcb07e0632f3 100644 --- a/apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts +++ b/apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts @@ -2,14 +2,14 @@ import * as Effect from "effect/Effect"; import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts"; import * as AzureDevOpsCli from "./AzureDevOpsCli.ts"; -import * as SourceControlProvider from "./SourceControlProvider.ts"; +import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; import { combinedAuthOutput, firstSafeAuthLine, providerAuth, type SourceControlAuthProbeInput, type SourceControlCliDiscoverySpec, -} from "./SourceControlProviderDiscovery.ts"; +} from "@t3tools/source-control-core/server/discovery"; function parseAzureAuth(input: SourceControlAuthProbeInput) { const account = input.stdout.trim().split(/\r?\n/)[0]?.trim(); diff --git a/apps/server/src/sourceControl/BitbucketApi.ts b/apps/server/src/sourceControl/BitbucketApi.ts index fbff83dfedb5..f7fa5cc4568c 100644 --- a/apps/server/src/sourceControl/BitbucketApi.ts +++ b/apps/server/src/sourceControl/BitbucketApi.ts @@ -29,11 +29,11 @@ import { type NormalizedBitbucketPullRequestRecord, } from "./bitbucketPullRequests.ts"; import { collectUint8StreamText } from "@t3tools/provider-core/server/collectStreamText"; -import * as SourceControlProvider from "./SourceControlProvider.ts"; +import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; import * as ServerSettings from "../serverSettings.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; -import { retryAtFromHeader } from "./SourceControlRateLimit.ts"; +import { retryAtFromHeader } from "@t3tools/source-control-core/server/SourceControlRateLimit"; const DEFAULT_API_BASE_URL = "https://api.bitbucket.org/2.0"; /** A response body past this is cut short, so one huge diff cannot exhaust the server. */ diff --git a/apps/server/src/sourceControl/BitbucketSourceControlProvider.ts b/apps/server/src/sourceControl/BitbucketSourceControlProvider.ts index f1a01056fbd0..b9afd60bc389 100644 --- a/apps/server/src/sourceControl/BitbucketSourceControlProvider.ts +++ b/apps/server/src/sourceControl/BitbucketSourceControlProvider.ts @@ -4,8 +4,8 @@ import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contrac import * as BitbucketApi from "./BitbucketApi.ts"; import type { NormalizedBitbucketPullRequestRecord } from "./bitbucketPullRequests.ts"; -import * as SourceControlProvider from "./SourceControlProvider.ts"; -import type { SourceControlApiDiscoverySpec } from "./SourceControlProviderDiscovery.ts"; +import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; +import type { SourceControlApiDiscoverySpec } from "@t3tools/source-control-core/server/discovery"; function toChangeRequest(summary: NormalizedBitbucketPullRequestRecord): ChangeRequest { return { diff --git a/apps/server/src/sourceControl/ForgejoCli.ts b/apps/server/src/sourceControl/ForgejoCli.ts index eb284af237c8..573c081e5a42 100644 --- a/apps/server/src/sourceControl/ForgejoCli.ts +++ b/apps/server/src/sourceControl/ForgejoCli.ts @@ -15,7 +15,7 @@ import { decodeJsonResult } from "@t3tools/shared/schemaJson"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as VcsProcess from "../vcs/VcsProcess.ts"; import { collectUint8StreamText } from "@t3tools/provider-core/server/collectStreamText"; -import type { SourceControlProviderContext } from "./SourceControlProvider.ts"; +import type { SourceControlProviderContext } from "@t3tools/source-control-core/server/SourceControlProvider"; const encodeApiBody = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); diff --git a/apps/server/src/sourceControl/ForgejoSourceControlProvider.ts b/apps/server/src/sourceControl/ForgejoSourceControlProvider.ts index 5a501d96b235..79a1ca6f2aa0 100644 --- a/apps/server/src/sourceControl/ForgejoSourceControlProvider.ts +++ b/apps/server/src/sourceControl/ForgejoSourceControlProvider.ts @@ -5,13 +5,13 @@ import * as Result from "effect/Result"; import { SourceControlProviderError } from "@t3tools/contracts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; import * as ForgejoCli from "./ForgejoCli.ts"; -import * as SourceControlProvider from "./SourceControlProvider.ts"; +import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; import { providerAuth, probeSourceControlProvider, type SourceControlCliDiscoverySpec, type SourceControlManagedCliDiscoverySpec, -} from "./SourceControlProviderDiscovery.ts"; +} from "@t3tools/source-control-core/server/discovery"; import { ForgejoPullRequestSchema, toForgejoChangeRequest } from "./forgejoPullRequests.ts"; const isForgejoCliError = Schema.is(ForgejoCli.ForgejoCliError); diff --git a/apps/server/src/sourceControl/GitHubApi.test.ts b/apps/server/src/sourceControl/GitHubApi.test.ts index 60e2c6f1b45d..cd4c2fb384f2 100644 --- a/apps/server/src/sourceControl/GitHubApi.test.ts +++ b/apps/server/src/sourceControl/GitHubApi.test.ts @@ -13,7 +13,7 @@ import { HttpClient, HttpClientResponse, type HttpClientRequest } from "effect/h import * as GitHubApi from "./GitHubApi.ts"; import * as GitHubCredentials from "./GitHubCredentials.ts"; import * as GitHubQuota from "./githubQuota.ts"; -import * as SourceControlRateLimit from "./SourceControlRateLimit.ts"; +import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; import * as VcsProcess from "../vcs/VcsProcess.ts"; import * as ServerSettings from "../serverSettings.ts"; diff --git a/apps/server/src/sourceControl/GitHubApi.ts b/apps/server/src/sourceControl/GitHubApi.ts index 5a99cf68943b..1cc66a1323c7 100644 --- a/apps/server/src/sourceControl/GitHubApi.ts +++ b/apps/server/src/sourceControl/GitHubApi.ts @@ -13,7 +13,7 @@ import { HttpClient, HttpClientRequest, type HttpClientResponse } from "effect/h import { collectUint8StreamText } from "@t3tools/provider-core/server/collectStreamText"; import * as GitHubCredentials from "./GitHubCredentials.ts"; import * as GitHubQuota from "./githubQuota.ts"; -import * as SourceControlRateLimit from "./SourceControlRateLimit.ts"; +import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; const DEFAULT_TIMEOUT = Duration.seconds(30); const DEFAULT_MAX_RESPONSE_BYTES = 8 * 1024 * 1024; diff --git a/apps/server/src/sourceControl/GitHubSourceControlProvider.ts b/apps/server/src/sourceControl/GitHubSourceControlProvider.ts index da3d53330864..35876794048a 100644 --- a/apps/server/src/sourceControl/GitHubSourceControlProvider.ts +++ b/apps/server/src/sourceControl/GitHubSourceControlProvider.ts @@ -37,14 +37,14 @@ import { resolveGitHubRepository, type GitHubRepositoryLocator, } from "./gitHubRepositoryResolution.ts"; -import * as SourceControlRateLimit from "./SourceControlRateLimit.ts"; +import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; import { effectiveGitHubAccount, findAuthenticatedGitHubAccount, parseGitHubAuthStatus, type GitHubAuthStatusAccount, } from "./gitHubAuthStatus.ts"; -import * as SourceControlProvider from "./SourceControlProvider.ts"; +import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; import { combinedAuthOutput, firstSafeAuthLine, @@ -53,7 +53,7 @@ import { type SourceControlAuthProbeInput, type SourceControlCliDiscoverySpec, type SourceControlManagedCliDiscoverySpec, -} from "./SourceControlProviderDiscovery.ts"; +} from "@t3tools/source-control-core/server/discovery"; import * as VcsProcess from "../vcs/VcsProcess.ts"; const decodeLinkSubject = Schema.decodeUnknownEffect( diff --git a/apps/server/src/sourceControl/GitLabCli.ts b/apps/server/src/sourceControl/GitLabCli.ts index 9d03e1ab8aa5..2ec69a527fed 100644 --- a/apps/server/src/sourceControl/GitLabCli.ts +++ b/apps/server/src/sourceControl/GitLabCli.ts @@ -18,7 +18,7 @@ import { decodeGitLabMergeRequestJson, decodeGitLabMergeRequestListJson, } from "./gitLabMergeRequests.ts"; -import type * as SourceControlProvider from "./SourceControlProvider.ts"; +import type * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; const DEFAULT_TIMEOUT_MS = 30_000; diff --git a/apps/server/src/sourceControl/GitLabSourceControlProvider.ts b/apps/server/src/sourceControl/GitLabSourceControlProvider.ts index 00753bef7dea..ef63f11cfdf2 100644 --- a/apps/server/src/sourceControl/GitLabSourceControlProvider.ts +++ b/apps/server/src/sourceControl/GitLabSourceControlProvider.ts @@ -4,7 +4,7 @@ import * as Option from "effect/Option"; import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts"; import * as GitLabCli from "./GitLabCli.ts"; -import * as SourceControlProvider from "./SourceControlProvider.ts"; +import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; import { combinedAuthOutput, firstSafeAuthLine, @@ -14,7 +14,7 @@ import { type SourceControlAuthProbeInput, type SourceControlCliDiscoverySpec, type SourceControlUnknownRemoteRefinementInput, -} from "./SourceControlProviderDiscovery.ts"; +} from "@t3tools/source-control-core/server/discovery"; import { findAuthenticatedGitLabHost, parseGitLabAuthStatusHosts } from "./gitLabAuthStatus.ts"; const decodeLinkSubject = Schema.decodeUnknownEffect( diff --git a/apps/server/src/sourceControl/ServerSourceControlHost.ts b/apps/server/src/sourceControl/ServerSourceControlHost.ts new file mode 100644 index 000000000000..f458795ab76e --- /dev/null +++ b/apps/server/src/sourceControl/ServerSourceControlHost.ts @@ -0,0 +1,24 @@ +/** + * The server's implementation of `SourceControlHost.SourceControlHost`, the only server surface + * source control provider packages may use. + * + * @module sourceControl/ServerSourceControlHost + */ +import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; + +import * as ServerSettings from "../serverSettings.ts"; +import * as VcsProcess from "../vcs/VcsProcess.ts"; + +export const layer = Layer.effect( + SourceControlHost.SourceControlHost, + Effect.gen(function* () { + const serverSettings = yield* ServerSettings.ServerSettingsService; + const process = yield* VcsProcess.VcsProcess; + return SourceControlHost.SourceControlHost.of({ + settings: { get: serverSettings.getSettings }, + process: { run: process.run }, + }); + }), +); diff --git a/apps/server/src/sourceControl/SourceControlDiscovery.test.ts b/apps/server/src/sourceControl/SourceControlDiscovery.test.ts index 6101d148a632..e0d1c3efca23 100644 --- a/apps/server/src/sourceControl/SourceControlDiscovery.test.ts +++ b/apps/server/src/sourceControl/SourceControlDiscovery.test.ts @@ -25,7 +25,8 @@ import * as ForgejoSourceControlProvider from "./ForgejoSourceControlProvider.ts import * as ForgejoPullRequestProvider from "../pullRequest/ForgejoPullRequestProvider.ts"; import * as SourceControlDiscovery from "./SourceControlDiscovery.ts"; import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; -import { firstNonEmptyLine } from "./SourceControlProviderDiscovery.ts"; +import * as ServerSourceControlHost from "./ServerSourceControlHost.ts"; +import { firstNonEmptyLine } from "@t3tools/source-control-core/server/discovery"; const layerSourceControlProviderRegistryTest = (input: { readonly bitbucket: Partial; @@ -46,6 +47,14 @@ const layerSourceControlProviderRegistryTest = (input: { Layer.mock(ForgejoCli.ForgejoCli)({ listLogins: () => Effect.succeed([]) }), Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({}), Layer.mock(VcsProcess.VcsProcess)(input.process), + ServerSourceControlHost.layer.pipe( + Layer.provide( + Layer.merge( + Layer.mock(VcsProcess.VcsProcess)(input.process), + ServerSettings.ServerSettingsService.layerTest(), + ), + ), + ), ), ), ); diff --git a/apps/server/src/sourceControl/SourceControlDiscovery.ts b/apps/server/src/sourceControl/SourceControlDiscovery.ts index e178148f131f..f56985fefd63 100644 --- a/apps/server/src/sourceControl/SourceControlDiscovery.ts +++ b/apps/server/src/sourceControl/SourceControlDiscovery.ts @@ -10,7 +10,7 @@ import * as Option from "effect/Option"; import * as ServerConfig from "../config.ts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; -import { detailFromCause, firstNonEmptyLine } from "./SourceControlProviderDiscovery.ts"; +import { detailFromCause, firstNonEmptyLine } from "@t3tools/source-control-core/server/discovery"; import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; interface DiscoveryProbe { diff --git a/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts b/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts index 705c38b847b7..08a7259eb1e1 100644 --- a/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts +++ b/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts @@ -21,6 +21,7 @@ import * as GitHubCredentials from "./GitHubCredentials.ts"; import * as GitLabCli from "./GitLabCli.ts"; import * as ForgejoCli from "./ForgejoCli.ts"; import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; +import * as ServerSourceControlHost from "./ServerSourceControlHost.ts"; const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z"); @@ -96,6 +97,11 @@ function makeRegistry(input: { NodeServices.layer, layerRegistry, layerProcess, + ServerSourceControlHost.layer.pipe( + Layer.provide( + Layer.merge(layerProcess, ServerSettings.ServerSettingsService.layerTest()), + ), + ), Layer.mock(AzureDevOpsCli.AzureDevOpsCli)({}), Layer.mock(BitbucketApi.BitbucketApi)({}), ServerSettings.ServerSettingsService.layerTest(), diff --git a/apps/server/src/sourceControl/SourceControlProviderRegistry.ts b/apps/server/src/sourceControl/SourceControlProviderRegistry.ts index 7a626b13f50f..309f79b1509f 100644 --- a/apps/server/src/sourceControl/SourceControlProviderRegistry.ts +++ b/apps/server/src/sourceControl/SourceControlProviderRegistry.ts @@ -16,15 +16,16 @@ import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvide import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts"; import * as ForgejoSourceControlProvider from "./ForgejoSourceControlProvider.ts"; -import * as SourceControlProvider from "./SourceControlProvider.ts"; +import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; import { probeSourceControlProvider, refineUnknownRemoteProvider, type SourceControlProviderDiscoverySpec, -} from "./SourceControlProviderDiscovery.ts"; +} from "@t3tools/source-control-core/server/discovery"; +import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; + import * as ServerConfig from "../config.ts"; import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; -import * as VcsProcess from "../vcs/VcsProcess.ts"; const PROVIDER_DETECTION_CACHE_CAPACITY = 2_048; const PROVIDER_DETECTION_CACHE_TTL = Duration.seconds(5); @@ -201,7 +202,7 @@ function bindProviderContext( export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWithProviders")( function* (registrations: ReadonlyArray) { const config = yield* ServerConfig.ServerConfig; - const process = yield* VcsProcess.VcsProcess; + const { process } = yield* SourceControlHost.SourceControlHost; const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry; const providers = new Map< SourceControlProviderKind, diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts index 3495b2270b3e..8aba02284403 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts @@ -15,7 +15,7 @@ import * as ServerConfig from "../config.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as BitbucketApi from "./BitbucketApi.ts"; import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; -import type * as SourceControlProvider from "./SourceControlProvider.ts"; +import type * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; import * as SourceControlRepositoryService from "./SourceControlRepositoryService.ts"; diff --git a/apps/server/src/sourceControl/githubQuota.test.ts b/apps/server/src/sourceControl/githubQuota.test.ts index e21970cecca7..e9c1fac93eb0 100644 --- a/apps/server/src/sourceControl/githubQuota.test.ts +++ b/apps/server/src/sourceControl/githubQuota.test.ts @@ -3,7 +3,7 @@ import * as Effect from "effect/Effect"; import * as TestClock from "effect/testing/TestClock"; import * as GitHubQuota from "./githubQuota.ts"; -import { CredentialScope } from "./SourceControlRateLimit.ts"; +import { CredentialScope } from "@t3tools/source-control-core/server/SourceControlRateLimit"; const RESET = Date.parse("2099-08-13T14:00:00Z"); const headers = (remaining: number, resource = "graphql", reset = RESET) => ({ diff --git a/apps/server/src/sourceControl/githubQuota.ts b/apps/server/src/sourceControl/githubQuota.ts index c553535019f4..642c48fddc9a 100644 --- a/apps/server/src/sourceControl/githubQuota.ts +++ b/apps/server/src/sourceControl/githubQuota.ts @@ -4,7 +4,7 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Ref from "effect/Ref"; -import * as SourceControlRateLimit from "./SourceControlRateLimit.ts"; +import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; /** The share of each quota a background read leaves for a user's next click. */ const RESERVE_RATIO = 0.1; diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index a035d6971d45..666805e1d79e 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -235,6 +235,7 @@ import * as GitHubApi from "./sourceControl/GitHubApi.ts"; import * as GitLabCli from "./sourceControl/GitLabCli.ts"; import * as ForgejoCli from "./sourceControl/ForgejoCli.ts"; import * as SourceControlProviderRegistry from "./sourceControl/SourceControlProviderRegistry.ts"; +import * as ServerSourceControlHost from "./sourceControl/ServerSourceControlHost.ts"; import * as GitVcsDriver from "./vcs/GitVcsDriver.ts"; import * as VcsDriverRegistry from "./vcs/VcsDriverRegistry.ts"; import * as VcsProjectConfig from "./vcs/VcsProjectConfig.ts"; @@ -3209,6 +3210,7 @@ export const layer = Layer.unwrap( GitHubApi.layerWithDependencies, GitLabCli.layer, ForgejoCli.layer, + ServerSourceControlHost.layer, ), ), Layer.provideMerge(GitVcsDriver.layer), diff --git a/docs/internals/pull-request-file-revisions.md b/docs/internals/pull-request-file-revisions.md index 3a84b8de665d..22eeb63848ea 100644 --- a/docs/internals/pull-request-file-revisions.md +++ b/docs/internals/pull-request-file-revisions.md @@ -17,7 +17,7 @@ a project the token cannot see. Below the boundary, in a patch parser or a response decoder, a missing path means this revision does not carry that file. Above it, in -[`ProviderFileRevisions`](../../apps/server/src/pullRequest/PullRequestProvider.ts), a missing path +[`ProviderFileRevisions`](../../packages/source-control-core/src/server/PullRequestProvider.ts), a missing path means the read could not say. A provider's `getFileRevisions` converts on the way up: a path its host looked at and has no version for arrives as the empty string, and absence is kept for what the provider never got to look at. Absence below the boundary does not survive it. diff --git a/package.json b/package.json index 55869fc4b391..471e5361eef2 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "tc": "vp run -r --concurrency-limit 2 typecheck", "lint": "vp lint --report-unused-disable-directives", "knip": "knip --preprocessor ./scripts/knip-schemas.ts", - "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-acp --workspace packages/provider-acp-registry --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --workspace packages/provider-cursor --workspace packages/provider-grok --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", + "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-acp --workspace packages/provider-acp-registry --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --workspace packages/provider-cursor --workspace packages/provider-grok --workspace packages/source-control-core --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", "knip:production": "knip --production --preprocessor ./scripts/knip-schemas.ts", "lint:mobile": "node scripts/mobile-native-static-check.ts", "test": "vp run -r test", diff --git a/packages/source-control-core/package.json b/packages/source-control-core/package.json new file mode 100644 index 000000000000..58742f9f3589 --- /dev/null +++ b/packages/source-control-core/package.json @@ -0,0 +1,48 @@ +{ + "name": "@t3tools/source-control-core", + "private": true, + "type": "module", + "exports": { + "./server/discovery": { + "types": "./src/server/discovery.ts", + "import": "./src/server/discovery.ts" + }, + "./server/mergeMessage": { + "types": "./src/server/mergeMessage.ts", + "import": "./src/server/mergeMessage.ts" + }, + "./server/pullRequestChecks": { + "types": "./src/server/pullRequestChecks.ts", + "import": "./src/server/pullRequestChecks.ts" + }, + "./server/PullRequestProvider": { + "types": "./src/server/PullRequestProvider.ts", + "import": "./src/server/PullRequestProvider.ts" + }, + "./server/SourceControlHost": { + "types": "./src/server/SourceControlHost.ts", + "import": "./src/server/SourceControlHost.ts" + }, + "./server/SourceControlProvider": { + "types": "./src/server/SourceControlProvider.ts", + "import": "./src/server/SourceControlProvider.ts" + }, + "./server/SourceControlRateLimit": { + "types": "./src/server/SourceControlRateLimit.ts", + "import": "./src/server/SourceControlRateLimit.ts" + } + }, + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vp test run --config ../../vite.config.ts --dir ." + }, + "dependencies": { + "@t3tools/contracts": "workspace:*", + "effect": "catalog:" + }, + "devDependencies": { + "@effect/vitest": "catalog:", + "@types/node": "catalog:", + "vite-plus": "catalog:" + } +} diff --git a/apps/server/src/pullRequest/PullRequestProvider.ts b/packages/source-control-core/src/server/PullRequestProvider.ts similarity index 100% rename from apps/server/src/pullRequest/PullRequestProvider.ts rename to packages/source-control-core/src/server/PullRequestProvider.ts diff --git a/packages/source-control-core/src/server/SourceControlHost.ts b/packages/source-control-core/src/server/SourceControlHost.ts new file mode 100644 index 000000000000..d779f86fe0e2 --- /dev/null +++ b/packages/source-control-core/src/server/SourceControlHost.ts @@ -0,0 +1,59 @@ +/** + * SourceControlHost — what a source control provider package may ask of the server it runs in. + * + * Providers run inside a T3 server but must not import it. The server provides this one + * service; everything a provider needs from its environment (settings, the process runner) + * goes through it, so a provider package depends only on `@t3tools/source-control-core` and + * its own API and CLI code. HTTP, the filesystem, and paths come from Effect's platform + * services directly. + * + * @module source-control-core/server/SourceControlHost + */ +import type { ServerSettings, ServerSettingsError, VcsError } from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import type * as Effect from "effect/Effect"; +import type * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; + +/** One CLI invocation. The server bounds concurrency, output, and time for every run. */ +export interface SourceControlProcessInput { + readonly operation: string; + readonly command: string; + readonly args: ReadonlyArray; + readonly cwd: string; + readonly spawnCwd?: string; + readonly stdin?: string; + readonly onStdoutChunk?: (chunk: Uint8Array) => void; + readonly env?: NodeJS.ProcessEnv; + readonly allowNonZeroExit?: boolean; + readonly timeoutMs?: number; + readonly maxOutputBytes?: number; + /** What happens past `maxOutputBytes`: fail the run, or keep the first bytes. */ + readonly outputMode?: "error" | "truncate" | undefined; + readonly appendTruncationMarker?: boolean; +} + +export interface SourceControlProcessOutput { + readonly exitCode: ChildProcessSpawner.ExitCode; + readonly stdout: string; + readonly stderr: string; + readonly stdoutTruncated: boolean; + readonly stderrTruncated: boolean; + /** Present on real process output; optional so narrow test doubles remain lightweight. */ + readonly stdoutInvalidUtf8?: boolean; + readonly stderrInvalidUtf8?: boolean; +} + +export class SourceControlHost extends Context.Service< + SourceControlHost, + { + readonly settings: { + /** Read fresh on each call, so a credential saved in Settings applies without a restart. */ + readonly get: Effect.Effect; + }; + readonly process: { + readonly run: ( + input: SourceControlProcessInput, + ) => Effect.Effect; + }; + } +>()("@t3tools/source-control-core/server/SourceControlHost") {} diff --git a/apps/server/src/sourceControl/SourceControlProvider.test.ts b/packages/source-control-core/src/server/SourceControlProvider.test.ts similarity index 100% rename from apps/server/src/sourceControl/SourceControlProvider.test.ts rename to packages/source-control-core/src/server/SourceControlProvider.test.ts diff --git a/apps/server/src/sourceControl/SourceControlProvider.ts b/packages/source-control-core/src/server/SourceControlProvider.ts similarity index 98% rename from apps/server/src/sourceControl/SourceControlProvider.ts rename to packages/source-control-core/src/server/SourceControlProvider.ts index ec61691fdd42..2bcabceb00e3 100644 --- a/apps/server/src/sourceControl/SourceControlProvider.ts +++ b/packages/source-control-core/src/server/SourceControlProvider.ts @@ -142,4 +142,4 @@ export class SourceControlProvider extends Context.Service< readonly force?: boolean; }) => Effect.Effect; } ->()("t3/sourceControl/SourceControlProvider") {} +>()("@t3tools/source-control-core/server/SourceControlProvider") {} diff --git a/apps/server/src/sourceControl/SourceControlRateLimit.test.ts b/packages/source-control-core/src/server/SourceControlRateLimit.test.ts similarity index 100% rename from apps/server/src/sourceControl/SourceControlRateLimit.test.ts rename to packages/source-control-core/src/server/SourceControlRateLimit.test.ts diff --git a/apps/server/src/sourceControl/SourceControlRateLimit.ts b/packages/source-control-core/src/server/SourceControlRateLimit.ts similarity index 95% rename from apps/server/src/sourceControl/SourceControlRateLimit.ts rename to packages/source-control-core/src/server/SourceControlRateLimit.ts index dc6242a60eb6..f2ef07dd2529 100644 --- a/apps/server/src/sourceControl/SourceControlRateLimit.ts +++ b/packages/source-control-core/src/server/SourceControlRateLimit.ts @@ -13,9 +13,12 @@ import { const FALLBACK_COOLDOWN = Duration.seconds(30); const MAX_FALLBACK_COOLDOWN = Duration.minutes(15); -export const CredentialScope = Context.Reference("t3/sourceControl/CredentialScope", { - defaultValue: () => "", -}); +export const CredentialScope = Context.Reference( + "@t3tools/source-control-core/server/SourceControlRateLimit/CredentialScope", + { + defaultValue: () => "", + }, +); interface RateLimitKey { readonly provider: SourceControlProviderKind; @@ -61,7 +64,7 @@ export class SourceControlRateLimit extends Context.Service< ) => Effect.Effect; readonly recordSuccess: (input: RateLimitLease) => Effect.Effect; } ->()("t3/sourceControl/SourceControlRateLimit") {} +>()("@t3tools/source-control-core/server/SourceControlRateLimit") {} function normalizedKey(key: RateLimitKey, scope: string): string { return `${key.provider}\0${key.host.trim().toLowerCase()}\0${scope}`; diff --git a/apps/server/src/sourceControl/SourceControlProviderDiscovery.ts b/packages/source-control-core/src/server/discovery.ts similarity index 95% rename from apps/server/src/sourceControl/SourceControlProviderDiscovery.ts rename to packages/source-control-core/src/server/discovery.ts index 466e66230649..70d26b4a963c 100644 --- a/apps/server/src/sourceControl/SourceControlProviderDiscovery.ts +++ b/packages/source-control-core/src/server/discovery.ts @@ -1,3 +1,8 @@ +/** + * Discovery specs: how each provider reports its CLI or API, and the shared probe that runs them. + * + * @module source-control-core/server/discovery + */ import * as NodeUtil from "node:util"; import type { SourceControlProviderAuth, @@ -9,12 +14,12 @@ import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import type * as SourceControlProvider from "./SourceControlProvider.ts"; -import type * as VcsProcess from "../vcs/VcsProcess.ts"; +import type * as SourceControlHost from "./SourceControlHost.ts"; export interface SourceControlAuthProbeInput { readonly stdout: string; readonly stderr: string; - readonly exitCode: VcsProcess.VcsProcessOutput["exitCode"]; + readonly exitCode: SourceControlHost.SourceControlProcessOutput["exitCode"]; } export interface SourceControlUnknownRemoteRefinementInput { @@ -179,7 +184,7 @@ function isCliRemoteRefinementSpec( function probeCli(input: { readonly spec: SourceControlCliDiscoverySpec; - readonly process: VcsProcess.VcsProcess["Service"]; + readonly process: SourceControlHost.SourceControlHost["Service"]["process"]; readonly cwd: string; }): Effect.Effect { return input.process @@ -223,7 +228,7 @@ function probeCli(input: { export function probeSourceControlProvider(input: { readonly spec: SourceControlProviderDiscoverySpec; - readonly process: VcsProcess.VcsProcess["Service"]; + readonly process: SourceControlHost.SourceControlHost["Service"]["process"]; readonly cwd: string; }): Effect.Effect { if (input.spec.type === "managed-cli") return input.spec.probe(input.cwd); @@ -292,7 +297,7 @@ export function probeSourceControlProvider(input: { export const refineUnknownRemoteProvider = Effect.fn("refineUnknownRemoteProvider")( function* (input: { readonly specs: ReadonlyArray; - readonly process: VcsProcess.VcsProcess["Service"]; + readonly process: SourceControlHost.SourceControlHost["Service"]["process"]; readonly cwd: string; readonly context: SourceControlProvider.SourceControlProviderContext | null; }): Effect.fn.Return { diff --git a/apps/server/src/pullRequest/mergeMessage.test.ts b/packages/source-control-core/src/server/mergeMessage.test.ts similarity index 100% rename from apps/server/src/pullRequest/mergeMessage.test.ts rename to packages/source-control-core/src/server/mergeMessage.test.ts diff --git a/apps/server/src/pullRequest/mergeMessage.ts b/packages/source-control-core/src/server/mergeMessage.ts similarity index 100% rename from apps/server/src/pullRequest/mergeMessage.ts rename to packages/source-control-core/src/server/mergeMessage.ts diff --git a/apps/server/src/pullRequest/pullRequestChecks.test.ts b/packages/source-control-core/src/server/pullRequestChecks.test.ts similarity index 100% rename from apps/server/src/pullRequest/pullRequestChecks.test.ts rename to packages/source-control-core/src/server/pullRequestChecks.test.ts diff --git a/apps/server/src/pullRequest/pullRequestChecks.ts b/packages/source-control-core/src/server/pullRequestChecks.ts similarity index 100% rename from apps/server/src/pullRequest/pullRequestChecks.ts rename to packages/source-control-core/src/server/pullRequestChecks.ts diff --git a/packages/source-control-core/tsconfig.json b/packages/source-control-core/tsconfig.json new file mode 100644 index 000000000000..374bac55202d --- /dev/null +++ b/packages/source-control-core/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5790c62cc13d..387677cd3d52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -648,6 +648,9 @@ importers: '@t3tools/provider-pi': specifier: workspace:* version: link:../../packages/provider-pi + '@t3tools/source-control-core': + specifier: workspace:* + version: link:../../packages/source-control-core diff: specifier: 8.0.3 version: 8.0.3 @@ -1510,6 +1513,25 @@ importers: specifier: 1.0.0 version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + packages/source-control-core: + dependencies: + '@t3tools/contracts': + specifier: workspace:* + version: link:../contracts + effect: + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) + devDependencies: + '@effect/vitest': + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + '@types/node': + specifier: 24.12.4 + version: 24.12.4 + vite-plus: + specifier: 1.0.0 + version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + packages/ssh: dependencies: '@t3tools/contracts': diff --git a/scripts/release-smoke.ts b/scripts/release-smoke.ts index fd8921b67c12..f1c903bc8fe1 100644 --- a/scripts/release-smoke.ts +++ b/scripts/release-smoke.ts @@ -37,6 +37,7 @@ const workspaceFiles = [ "packages/provider-grok/package.json", "packages/provider-muse/package.json", "packages/provider-opencode/package.json", + "packages/source-control-core/package.json", "packages/effect-acp/package.json", "packages/effect-codex-app-server/package.json", "scripts/package.json", From f6e45028efb69d208a45aee8c2fa514adde44977 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 14:50:00 -0700 Subject: [PATCH 11/39] refactor(source-control): Forgejo lives in @t3tools/source-control-forgejo (#17581) Co-authored-by: Claude Opus 5.5 --- apps/server/package.json | 1 + apps/server/scripts/evaluate-thread-titles.ts | 6 +- apps/server/src/git/GitManager.test.ts | 2 +- .../pullRequest/BitbucketPullRequestApi.ts | 2 +- .../PullRequestProviderRegistry.ts | 22 +- .../pullRequest/PullRequestService.test.ts | 4 +- apps/server/src/server.ts | 7 +- .../SourceControlDiscovery.test.ts | 1290 +---------------- .../SourceControlProviderRegistry.test.ts | 2 +- .../SourceControlProviderRegistry.ts | 15 +- .../src/sourceControl/builtInDrivers.ts | 20 + apps/server/src/ws.ts | 6 +- package.json | 2 +- packages/source-control-core/package.json | 9 + .../src/server/diffFileRevisions.test.ts | 2 +- .../src/server/diffFileRevisions.ts | 0 .../source-control-core/src/server/driver.ts | 36 + packages/source-control-forgejo/package.json | 48 + .../src/server}/ForgejoCli.ts | 10 +- .../src/server}/ForgejoPullRequestProvider.ts | 4 +- .../ForgejoSourceControlProvider.test.ts | 1283 ++++++++++++++++ .../server}/ForgejoSourceControlProvider.ts | 6 +- .../src/server/driver.ts | 20 + .../src/server}/forgejoPullRequestJson.ts | 0 .../src/server}/forgejoPullRequests.ts | 0 packages/source-control-forgejo/tsconfig.json | 7 + packages/source-control-testing/package.json | 22 + .../src/TestSourceControlHost.ts | 47 + packages/source-control-testing/tsconfig.json | 7 + pnpm-lock.yaml | 53 + scripts/release-smoke.ts | 2 + 31 files changed, 1632 insertions(+), 1303 deletions(-) create mode 100644 apps/server/src/sourceControl/builtInDrivers.ts rename apps/server/src/pullRequest/bitbucketDiffRevisions.test.ts => packages/source-control-core/src/server/diffFileRevisions.test.ts (99%) rename apps/server/src/pullRequest/bitbucketDiffRevisions.ts => packages/source-control-core/src/server/diffFileRevisions.ts (100%) create mode 100644 packages/source-control-core/src/server/driver.ts create mode 100644 packages/source-control-forgejo/package.json rename {apps/server/src/sourceControl => packages/source-control-forgejo/src/server}/ForgejoCli.ts (98%) rename {apps/server/src/pullRequest => packages/source-control-forgejo/src/server}/ForgejoPullRequestProvider.ts (99%) create mode 100644 packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts rename {apps/server/src/sourceControl => packages/source-control-forgejo/src/server}/ForgejoSourceControlProvider.ts (98%) create mode 100644 packages/source-control-forgejo/src/server/driver.ts rename {apps/server/src/pullRequest => packages/source-control-forgejo/src/server}/forgejoPullRequestJson.ts (100%) rename {apps/server/src/sourceControl => packages/source-control-forgejo/src/server}/forgejoPullRequests.ts (100%) create mode 100644 packages/source-control-forgejo/tsconfig.json create mode 100644 packages/source-control-testing/package.json create mode 100644 packages/source-control-testing/src/TestSourceControlHost.ts create mode 100644 packages/source-control-testing/tsconfig.json diff --git a/apps/server/package.json b/apps/server/package.json index ea7b9019e415..17b77a22244a 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -46,6 +46,7 @@ "@t3tools/provider-opencode": "workspace:*", "@t3tools/provider-pi": "workspace:*", "@t3tools/source-control-core": "workspace:*", + "@t3tools/source-control-forgejo": "workspace:*", "diff": "8.0.3", "effect": "catalog:", "jose": "catalog:", diff --git a/apps/server/scripts/evaluate-thread-titles.ts b/apps/server/scripts/evaluate-thread-titles.ts index 196bcbdfbcaa..3ebd7cbaf8d5 100644 --- a/apps/server/scripts/evaluate-thread-titles.ts +++ b/apps/server/scripts/evaluate-thread-titles.ts @@ -23,10 +23,9 @@ import { } from "@t3tools/provider-core/server/threadTitleContext"; import * as ThreadTitleLinks from "../src/textGeneration/ThreadTitleLinks.ts"; import * as SourceControlProviderRegistry from "../src/sourceControl/SourceControlProviderRegistry.ts"; -import * as ServerSourceControlHost from "../src/sourceControl/ServerSourceControlHost.ts"; import * as GitHubApi from "../src/sourceControl/GitHubApi.ts"; import * as GitLabCli from "../src/sourceControl/GitLabCli.ts"; -import * as ForgejoCli from "../src/sourceControl/ForgejoCli.ts"; +import * as SourceControlBuiltInDrivers from "../src/sourceControl/builtInDrivers.ts"; import * as AzureDevOpsCli from "../src/sourceControl/AzureDevOpsCli.ts"; import * as BitbucketApi from "../src/sourceControl/BitbucketApi.ts"; import * as ServerSettings from "../src/serverSettings.ts"; @@ -155,11 +154,10 @@ await Effect.runPromise( Layer.mergeAll( GitHubApi.layerWithDependencies, GitLabCli.layer, - ForgejoCli.layer, + SourceControlBuiltInDrivers.layer, AzureDevOpsCli.layer, // No saved credentials here; Bitbucket falls back to T3CODE_BITBUCKET_* variables. BitbucketApi.layer, - ServerSourceControlHost.layer, ), ), // Default settings: no saved Bitbucket token, gh's own GitHub account choice. diff --git a/apps/server/src/git/GitManager.test.ts b/apps/server/src/git/GitManager.test.ts index c7e76f5e71ab..9df266d56c8f 100644 --- a/apps/server/src/git/GitManager.test.ts +++ b/apps/server/src/git/GitManager.test.ts @@ -57,7 +57,7 @@ import * as GitLabSourceControlProvider from "../sourceControl/GitLabSourceContr import { ForgejoPullRequestSchema, toForgejoChangeRequest, -} from "../sourceControl/forgejoPullRequests.ts"; +} from "@t3tools/source-control-forgejo/server/forgejoPullRequests"; import type { SourceControlProvider } from "@t3tools/source-control-core/server/SourceControlProvider"; import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts"; import * as ServerConfig from "../config.ts"; diff --git a/apps/server/src/pullRequest/BitbucketPullRequestApi.ts b/apps/server/src/pullRequest/BitbucketPullRequestApi.ts index f5e108b05d45..b62b86ffe48f 100644 --- a/apps/server/src/pullRequest/BitbucketPullRequestApi.ts +++ b/apps/server/src/pullRequest/BitbucketPullRequestApi.ts @@ -22,7 +22,7 @@ import type { } from "@t3tools/contracts"; import * as BitbucketApi from "../sourceControl/BitbucketApi.ts"; -import { parseDiffFileRevisions } from "./bitbucketDiffRevisions.ts"; +import { parseDiffFileRevisions } from "@t3tools/source-control-core/server/diffFileRevisions"; import { buildReviewThreads, decodeCommentsJson, diff --git a/apps/server/src/pullRequest/PullRequestProviderRegistry.ts b/apps/server/src/pullRequest/PullRequestProviderRegistry.ts index 8e5a1d9cfd0b..3c2ad367d446 100644 --- a/apps/server/src/pullRequest/PullRequestProviderRegistry.ts +++ b/apps/server/src/pullRequest/PullRequestProviderRegistry.ts @@ -7,8 +7,6 @@ import * as AzureDevOpsCli from "../sourceControl/AzureDevOpsCli.ts"; import * as BitbucketApi from "../sourceControl/BitbucketApi.ts"; import * as GitHubApi from "../sourceControl/GitHubApi.ts"; import * as GitLabCli from "../sourceControl/GitLabCli.ts"; -import * as ForgejoCli from "../sourceControl/ForgejoCli.ts"; -import * as ForgejoPullRequestProvider from "./ForgejoPullRequestProvider.ts"; import * as AzureDevOpsPullRequestCli from "./AzureDevOpsPullRequestCli.ts"; import * as AzureDevOpsPullRequestProvider from "./AzureDevOpsPullRequestProvider.ts"; import * as BitbucketPullRequestApi from "./BitbucketPullRequestApi.ts"; @@ -17,6 +15,7 @@ import * as GitHubPullRequestApi from "./GitHubPullRequestApi.ts"; import * as GitHubPullRequestProvider from "./GitHubPullRequestProvider.ts"; import * as GitLabPullRequestCli from "./GitLabPullRequestCli.ts"; import * as GitLabPullRequestProvider from "./GitLabPullRequestProvider.ts"; +import * as BuiltInDrivers from "../sourceControl/builtInDrivers.ts"; import type { PullRequestProviderApi } from "@t3tools/source-control-core/server/PullRequestProvider"; export class PullRequestProviderRegistry extends Context.Service< @@ -45,16 +44,21 @@ export function fromProviders( * * @public Service construction is part of the canonical Effect module API. */ -export const make = Effect.map( - Effect.all([ +export const make = Effect.gen(function* () { + const providers = yield* Effect.all([ GitHubPullRequestProvider.make, GitLabPullRequestProvider.make, - ForgejoPullRequestProvider.make, BitbucketPullRequestProvider.make, AzureDevOpsPullRequestProvider.make, - ]), - fromProviders, -); + ]); + const drivers = yield* Effect.forEach(BuiltInDrivers.BUILT_IN_SOURCE_CONTROL_DRIVERS, (driver) => + driver.make.pipe(Effect.map((instance) => instance.pullRequests)), + ); + return fromProviders([ + ...providers, + ...drivers.filter((provider): provider is PullRequestProviderApi => provider !== null), + ]); +}); export const layer = Layer.effect(PullRequestProviderRegistry, make).pipe( Layer.provide( @@ -66,7 +70,7 @@ export const layer = Layer.effect(PullRequestProviderRegistry, make).pipe( ), ), Layer.provide(GitLabPullRequestCli.layer.pipe(Layer.provide(GitLabCli.layer))), - Layer.provide(ForgejoCli.layer), + Layer.provide(BuiltInDrivers.layer), Layer.provide(BitbucketPullRequestApi.layer.pipe(Layer.provide(BitbucketApi.layer))), Layer.provide(AzureDevOpsPullRequestCli.layer.pipe(Layer.provide(AzureDevOpsCli.layer))), ); diff --git a/apps/server/src/pullRequest/PullRequestService.test.ts b/apps/server/src/pullRequest/PullRequestService.test.ts index ef84de264ec6..83f362fa20a0 100644 --- a/apps/server/src/pullRequest/PullRequestService.test.ts +++ b/apps/server/src/pullRequest/PullRequestService.test.ts @@ -30,8 +30,8 @@ import * as PullRequestFilesViewed from "../persistence/PullRequestFilesViewed.t import * as RepositoryIdentityResolver from "../project/RepositoryIdentityResolver.ts"; import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts"; import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; -import * as ForgejoCli from "../sourceControl/ForgejoCli.ts"; -import * as ForgejoPullRequestProvider from "./ForgejoPullRequestProvider.ts"; +import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; +import * as ForgejoPullRequestProvider from "@t3tools/source-control-forgejo/server/ForgejoPullRequestProvider"; import { PullRequestProviderError, type ProviderChangeRequest, diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 913f8067d250..b1ac0b12dbd3 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -56,7 +56,8 @@ import * as AzureDevOpsCli from "./sourceControl/AzureDevOpsCli.ts"; import * as BitbucketApi from "./sourceControl/BitbucketApi.ts"; import * as GitHubApi from "./sourceControl/GitHubApi.ts"; import * as GitLabCli from "./sourceControl/GitLabCli.ts"; -import * as ForgejoCli from "./sourceControl/ForgejoCli.ts"; +import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; +import * as SourceControlBuiltInDrivers from "./sourceControl/builtInDrivers.ts"; import * as TextGeneration from "./textGeneration/TextGeneration.ts"; import * as ProviderInstanceRegistryHydration from "./provider/ProviderInstanceRegistryHydration.ts"; import * as TerminalManager from "./terminal/Manager.ts"; @@ -104,7 +105,6 @@ import * as ProjectCloneTracker from "./project/ProjectCloneTracker.ts"; import * as GitWorkflowService from "./git/GitWorkflowService.ts"; import * as ReviewService from "./review/ReviewService.ts"; import * as SourceControlProviderRegistry from "./sourceControl/SourceControlProviderRegistry.ts"; -import * as ServerSourceControlHost from "./sourceControl/ServerSourceControlHost.ts"; import * as PullRequestReadCache from "./pullRequest/PullRequestReadCache.ts"; import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts"; @@ -305,8 +305,7 @@ const layerSourceControlProviderRegistry = SourceControlProviderRegistry.layer.p BitbucketApi.layer, GitHubApi.layerWithDependencies, GitLabCli.layer, - ForgejoCli.layer, - ServerSourceControlHost.layer, + SourceControlBuiltInDrivers.layer, ), ), Layer.provideMerge(GitVcsDriver.layer), diff --git a/apps/server/src/sourceControl/SourceControlDiscovery.test.ts b/apps/server/src/sourceControl/SourceControlDiscovery.test.ts index e0d1c3efca23..fe7694cef545 100644 --- a/apps/server/src/sourceControl/SourceControlDiscovery.test.ts +++ b/apps/server/src/sourceControl/SourceControlDiscovery.test.ts @@ -20,13 +20,13 @@ import * as AzureDevOpsCli from "./AzureDevOpsCli.ts"; import * as BitbucketApi from "./BitbucketApi.ts"; import * as GitHubApi from "./GitHubApi.ts"; import * as GitLabCli from "./GitLabCli.ts"; -import * as ForgejoCli from "./ForgejoCli.ts"; -import * as ForgejoSourceControlProvider from "./ForgejoSourceControlProvider.ts"; -import * as ForgejoPullRequestProvider from "../pullRequest/ForgejoPullRequestProvider.ts"; +import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; +import * as ForgejoSourceControlProvider from "@t3tools/source-control-forgejo/server/ForgejoSourceControlProvider"; +import * as ForgejoPullRequestProvider from "@t3tools/source-control-forgejo/server/ForgejoPullRequestProvider"; import * as SourceControlDiscovery from "./SourceControlDiscovery.ts"; import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; import * as ServerSourceControlHost from "./ServerSourceControlHost.ts"; -import { firstNonEmptyLine } from "@t3tools/source-control-core/server/discovery"; +import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; const layerSourceControlProviderRegistryTest = (input: { readonly bitbucket: Partial; @@ -76,434 +76,6 @@ const processOutput = ( const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); const encodeJsonEffect = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); -it.effect("submits a Forgejo review without sending its summary in the preliminary GET", () => { - const methods: string[] = []; - const fetchReview = async ( - ...[input, init]: Parameters> - ) => { - const request = new Request(input instanceof Request ? input.url : String(input), { - ...(init?.method === undefined ? {} : { method: init.method }), - ...(init?.headers === undefined ? {} : { headers: init.headers }), - ...(init?.body === undefined ? {} : { body: init.body }), - }); - methods.push(request.method); - if (request.method === "GET") { - assert.strictEqual(request.url, "https://forgejo.test/api/v1/repos/maria/project/pulls/42"); - return new Response( - encodeJson({ - number: 42, - title: "Review target", - body: "", - html_url: "https://forgejo.test/maria/project/pulls/42", - user: { login: "maria" }, - state: "open", - merged: false, - head: { ref: "feature", sha: "head", repo: null }, - base: { ref: "main", sha: "base", repo: null }, - created_at: "2026-09-13T00:00:00Z", - updated_at: "2026-09-13T00:00:00Z", - closed_at: null, - merged_at: null, - labels: [], - }), - ); - } - assert.strictEqual(request.method, "POST"); - assert.strictEqual( - request.url, - "https://forgejo.test/api/v1/repos/maria/project/pulls/42/reviews", - ); - assert.deepStrictEqual(JSON.parse(await request.text()), { - event: "COMMENT", - body: "Review summary", - commit_id: "head", - comments: [], - }); - return new Response('{"id":1}', { status: 200 }); - }; - return Effect.gen(function* () { - const cli = yield* ForgejoCli.make; - const provider = yield* ForgejoPullRequestProvider.make.pipe( - Effect.provideService(ForgejoCli.ForgejoCli, cli), - ); - yield* provider.submitReview({ - cwd: "/repo", - repository: "maria/project", - host: "forgejo.test", - number: 42, - verdict: "comment", - body: "Review summary", - comments: [], - }); - assert.deepStrictEqual(methods, ["GET", "POST"]); - }).pipe( - Effect.provideService( - FetchHttpClient.Fetch, - Object.assign(fetchReview, { preconnect: () => undefined }), - ), - Effect.provide(FetchHttpClient.layer), - Effect.provideService( - FileSystem.FileSystem, - FileSystem.makeNoop({ - exists: () => Effect.succeed(true), - readFileString: () => - Effect.succeed( - encodeJson({ hosts: { "forgejo.test": { type: "Application", token: "test-token" } } }), - ), - }), - ), - Effect.provide( - Layer.mock(VcsProcess.VcsProcess)({ - run: (input) => { - if (input.command === "git") { - assert.deepStrictEqual(input.args, ["remote", "-v"]); - return Effect.succeed( - processOutput("origin\thttps://forgejo.test/maria/project.git (fetch)"), - ); - } - assert.strictEqual(input.command, "fj"); - assert.deepStrictEqual(input.args, ["--host", "https://forgejo.test", "whoami"]); - return Effect.succeed(processOutput("")); - }, - }), - ), - ); -}); - -it.effect("reads Forgejo checks without repository or viewer requests", () => { - const paths: string[] = []; - return Effect.gen(function* () { - const provider = yield* ForgejoPullRequestProvider.make; - const read = provider.getChangeRequestChecks; - if (read === undefined) return yield* Effect.die("checks read missing"); - const result = yield* read({ - cwd: "/repo", - repository: "acme/web", - host: "forgejo.test", - number: 1, - }); - assert.strictEqual(result.state, "open"); - assert.strictEqual(result.checks[0]?.status, "failure"); - assert.deepStrictEqual(paths, [ - "repos/acme/web/pulls/1", - "repos/acme/web/statuses/head?sort=recentupdate&limit=50&page=1", - "repos/acme/web/statuses/head?sort=recentupdate&limit=50&page=2", - ]); - }).pipe( - Effect.provide( - Layer.mock(ForgejoCli.ForgejoCli)({ - api: (input) => { - paths.push(input.path); - assert.match(input.path, /^repos\/acme\/web\/(pulls\/1|statuses\/head)/); - return Effect.succeed( - processOutput( - input.path.endsWith("pulls/1") - ? `{"number":1,"title":"Checks","body":"","html_url":"https://forgejo.test/acme/web/pulls/1", "user":null,"state":"open","merged":false, - "head":{"ref":"feature","sha":"head","repo":null},"base":{"ref":"main","sha":"base","repo":null}, - "created_at":"2026-09-16T00:00:00Z","updated_at":"2026-09-16T00:00:00Z","closed_at":null,"merged_at":null,"labels":[]}` - : input.path.endsWith("page=1") - ? `[{"context":"build","status":"failure","description":null,"target_url":null,"updated_at":"2026-09-16T00:00:00Z"}]` - : "[]", - ), - ); - }, - }), - ), - ); -}); - -it.effect.each([ - ["Ready", false, true, false, "mergeable"], - ["Draft", true, false, true, "unknown"], - ["Blocked", false, false, false, "unknown"], - ["Unchecked", false, undefined, false, "unknown"], - ["WIP: Legacy draft", undefined, false, true, "unknown"], - ["[WIP] Legacy draft", undefined, false, true, "unknown"], - ["WIP: Explicitly ready", false, false, false, "unknown"], -] as const)( - "reads Forgejo mergeability for %s (draft=%s, mergeable=%s) across list, summary and detail", - ([title, draft, mergeable, isDraft, mergeability]) => { - const pr = { - number: 42, - title, - body: "", - html_url: "https://forgejo.test/maria/project/pulls/42", - user: { login: "maria" }, - state: "open", - merged: false, - ...(draft === undefined ? {} : { draft }), - ...(mergeable === undefined ? {} : { mergeable }), - head: { ref: "feature", sha: "head", repo: null }, - base: { ref: "main", sha: "base", repo: null }, - created_at: "2026-09-16T00:00:00Z", - updated_at: "2026-09-16T00:00:00Z", - closed_at: null, - merged_at: null, - labels: [], - }; - return Effect.gen(function* () { - const provider = yield* ForgejoPullRequestProvider.make; - const readSummary = provider.getChangeRequestSummary; - if (readSummary === undefined) return yield* Effect.die("summary read missing"); - const input = { cwd: "/repo", repository: "maria/project", host: "forgejo.test", number: 42 }; - const list = yield* provider.listChangeRequests({ - ...input, - state: "open", - involvement: "all", - viewer: "maria", - limit: 10, - }); - const summary = yield* readSummary(input); - const detail = yield* provider.getChangeRequest(input); - assert.strictEqual(list.items.length, 1); - for (const result of [list.items[0]!, summary, detail]) { - assert.strictEqual(result.mergeability, mergeability); - assert.strictEqual(result.isDraft, isDraft); - } - }).pipe( - Effect.provide( - Layer.mock(ForgejoCli.ForgejoCli)({ - api: (input) => { - const [path, query] = input.path.split("?"); - let response: unknown; - switch (path) { - case "repos/maria/project/pulls": - response = new URLSearchParams(query).get("page") === "1" ? [pr] : []; - break; - case "repos/maria/project/pulls/42": - response = pr; - break; - case "repos/maria/project": - response = { full_name: "maria/project", permissions: { push: true, admin: true } }; - break; - case "user": - response = pr.user; - break; - case "repos/maria/project/statuses/head": - response = []; - break; - default: - return Effect.die(`Unexpected Forgejo request: ${input.path}`); - } - return encodeJsonEffect(response).pipe(Effect.orDie, Effect.map(processOutput)); - }, - }), - ), - ); - }, -); - -it.effect("loads Forgejo pull request references from files and commits views", () => - Effect.gen(function* () { - const provider = yield* ForgejoSourceControlProvider.make; - for (const reference of [ - "42", - "#42", - "https://forgejo.test/maria/project/pulls/42", - "https://forgejo.test/maria/project/pulls/42/", - "https://forgejo.test/maria/project/pulls/42/files?w=1#diff-1", - "http://forgejo.test:3000/git/maria/project/pulls/42/commits", - ]) { - const result = yield* provider.getChangeRequest({ cwd: "/repo", reference }); - assert.strictEqual(result.number, 42); - assert.strictEqual(result.title, "Forgejo view reference"); - } - const invalid = yield* provider - .getChangeRequest({ - cwd: "/repo", - reference: "https://forgejo.test/maria/project/pulls/42invalid/files", - }) - .pipe(Effect.result); - assert.strictEqual(invalid._tag, "Failure"); - }).pipe( - Effect.provide( - Layer.mergeAll( - Layer.succeed(FileSystem.FileSystem, FileSystem.makeNoop({})), - Layer.mock(VcsProcess.VcsProcess)({}), - Layer.mock(ForgejoCli.ForgejoCli)({ - resolveRepository: () => - Effect.succeed({ - login: "work", - repository: "maria/project", - baseUrl: "https://forgejo.test", - }), - api: (input) => { - assert.strictEqual(input.path, "repos/maria/project/pulls/42"); - return encodeJsonEffect({ - number: 42, - title: "Forgejo view reference", - html_url: "https://forgejo.test/maria/project/pulls/42", - state: "open", - merged: false, - base: { ref: "main", sha: "base", repo: null }, - head: { ref: "feature", sha: "head", repo: null }, - }).pipe(Effect.orDie, Effect.map(processOutput)); - }, - }), - ), - ), - ), -); - -it.effect( - "loads Forgejo reactions on comments, reviews and inline threads and resolves review mutations", - () => { - const user = { login: "maria" }; - const review = { - id: 8, - body: "review body", - user, - state: "COMMENT", - submitted_at: "2026-09-12T00:00:00Z", - html_url: "https://forgejo.test/maria/project/pulls/2#issuecomment-37", - comments_count: 1, - }; - const comment = { id: 12, body: "ordinary", user, created_at: review.submitted_at }; - const responses: Record = { - user, - "repos/maria/project/issues/2/comments": [comment], - "repos/maria/project/pulls/2/reviews": [review], - "repos/maria/project/pulls/2/reviews/8": review, - "repos/maria/project/pulls/2/reviews/9": { ...review, id: 9, html_url: "" }, - "repos/maria/project/pulls/2/commits": [], - "repos/maria/project/issues/2/reactions": [], - "repos/maria/project/pulls/2/reviews/8/comments": [ - { - ...comment, - id: 38, - body: "inline", - path: "file.ts", - position: 1, - original_position: 1, - commit_id: "head", - original_commit_id: "head", - resolver: null, - }, - ], - "repos/maria/project/issues/comments/12/reactions": [{ content: "+1", user }], - "repos/maria/project/issues/comments/37/reactions": [{ content: "heart", user }], - "repos/maria/project/issues/comments/38/reactions": [ - { content: "rocket", user: { login: "reviewer" } }, - ], - }; - const writes: ForgejoCli.ForgejoApiInput[] = []; - let reactionReads = 0; - return Effect.gen(function* () { - const provider = yield* ForgejoPullRequestProvider.make; - const input = { cwd: "/repo", repository: "maria/project", host: "forgejo.test", number: 2 }; - const activity = yield* provider.getChangeRequestActivity(input); - assert.deepStrictEqual( - activity.comments.map((entry) => ({ - id: entry.id, - kind: entry.kind, - reactions: entry.reactions, - })), - [ - { - id: "12", - kind: "issue-comment", - reactions: [{ content: "thumbs-up", count: 1, actors: [], viewerHasReacted: true }], - }, - { - id: "review:8", - kind: "review", - reactions: [{ content: "heart", count: 1, actors: [], viewerHasReacted: true }], - }, - { - id: "38", - kind: "review-comment", - reactions: [ - { content: "rocket", count: 1, actors: ["reviewer"], viewerHasReacted: false }, - ], - }, - ], - ); - const inlineComment = activity.comments[2]; - assert.ok(inlineComment); - assert.deepStrictEqual(activity.reviewThreads[0]?.comments, [inlineComment]); - for (const reacted of [true, false]) { - yield* provider.setReaction({ ...input, subjectId: "review:8", content: "heart", reacted }); - yield* provider.setReaction({ ...input, subjectId: "38", content: "rocket", reacted }); - } - assert.deepStrictEqual( - writes.map(({ path, method, body }) => ({ path, method, body })), - [ - { - path: "repos/maria/project/issues/comments/37/reactions", - method: "POST", - body: { content: "heart" }, - }, - { - path: "repos/maria/project/issues/comments/38/reactions", - method: "POST", - body: { content: "rocket" }, - }, - { - path: "repos/maria/project/issues/comments/37/reactions", - method: "DELETE", - body: { content: "heart" }, - }, - { - path: "repos/maria/project/issues/comments/38/reactions", - method: "DELETE", - body: { content: "rocket" }, - }, - ], - ); - const missing = yield* provider - .setReaction({ ...input, subjectId: "review:9", content: "heart", reacted: true }) - .pipe(Effect.result); - assert.strictEqual(missing._tag, "Failure"); - if (missing._tag === "Failure") assert.include(missing.failure.detail, "comment ID"); - assert.strictEqual(writes.length, 4); - responses["repos/maria/project/pulls/2/reviews/8/comments"] = Array.from( - { length: 501 }, - (_, index) => ({ - ...comment, - id: 1000 + index, - path: "file.ts", - position: 1, - original_position: 1, - commit_id: "head", - original_commit_id: "head", - resolver: null, - }), - ); - for (let index = 0; index < 500; index++) { - responses[`repos/maria/project/issues/comments/${1000 + index}/reactions`] = []; - } - reactionReads = 0; - const bounded = yield* provider.getChangeRequestActivity(input); - assert.strictEqual(bounded.reviewThreads.length, 500); - assert.strictEqual( - bounded.comments.filter((entry) => entry.kind === "review-comment").length, - 500, - ); - assert.strictEqual(bounded.commentsTruncated, true); - assert.strictEqual(reactionReads, 502); - }).pipe( - Effect.provide( - Layer.mock(ForgejoCli.ForgejoCli)({ - api: (input) => { - if (input.method) { - writes.push(input); - return Effect.succeed(processOutput("{}")); - } - const path = input.path.split("?")[0]!; - if (/\/issues\/comments\/\d+\/reactions$/.test(path)) reactionReads++; - assert.ok(Object.hasOwn(responses, path), `Unexpected Forgejo request: ${path}`); - const page = Number(new URLSearchParams(input.path.split("?")[1]).get("page")); - return encodeJsonEffect(page > 1 ? [] : responses[path]).pipe( - Effect.orDie, - Effect.map(processOutput), - ); - }, - }), - ), - ); - }, -); - it.effect("reports implemented tools separately from locally available executables", () => { const processMock = { run: (input: VcsProcess.VcsProcessInput) => { @@ -764,820 +336,6 @@ Logged in to gitlab.com as gitlab-user }).pipe(Effect.provide(layerTest)); }); -it.effect("discovers Forgejo accounts and retains the server port", () => - Effect.gen(function* () { - const auth = ForgejoSourceControlProvider.discovery.parseAuth( - processOutput( - yield* encodeJsonEffect([ - { - name: "work", - url: "http://forgejo.local:3000", - ssh_host: "git.forgejo.local", - user: "maria", - default: "true", - valid: "true", - }, - ]), - ), - ); - assert.deepStrictEqual( - firstNonEmptyLine("\u001b[1mtea version 0.16.0\u001b[0m\n"), - Option.some("tea version 0.16.0"), - ); - assert.strictEqual(auth.status, "authenticated"); - assert.deepStrictEqual(auth.account, Option.some("maria")); - assert.deepStrictEqual(auth.host, Option.some("forgejo.local:3000")); - const revoked = ForgejoSourceControlProvider.discovery.parseAuth( - processOutput( - encodeJson([ - { - name: "work", - url: "http://forgejo.local:3000", - user: "maria", - default: "true", - valid: "false", - }, - ]), - ), - ); - assert.strictEqual(revoked.status, "unauthenticated"); - const refined = ForgejoSourceControlProvider.discovery.refineUnknownRemote({ - cwd: "/repo", - context: { - provider: { - kind: "unknown", - name: "git.forgejo.local", - baseUrl: "https://git.forgejo.local", - }, - remoteName: "origin", - remoteUrl: "git@git.forgejo.local:maria/project.git", - }, - auth: processOutput( - yield* encodeJsonEffect([ - { - name: "work", - url: "http://forgejo.local:3000", - ssh_host: "git.forgejo.local", - user: "maria", - default: "true", - }, - ]), - ), - }); - assert.deepStrictEqual(refined, { - kind: "forgejo", - name: "Forgejo / Gitea", - baseUrl: "http://forgejo.local:3000", - }); - }), -); - -it.effect("does not choose a default Forgejo login across ambiguous SSH server ports", () => - Effect.gen(function* () { - const logins = ForgejoCli.parseForgejoLogins( - yield* encodeJsonEffect([ - { - name: "one", - url: "http://forgejo.local:3000", - ssh_host: "forgejo.local", - user: "maria", - default: "true", - }, - { - name: "two", - url: "http://forgejo.local:4000", - ssh_host: "forgejo.local", - user: "maria", - default: "false", - }, - ]), - ); - const remote = ForgejoCli.parseForgejoRemote("git@forgejo.local:maria/project.git"); - assert.isNotNull(remote); - assert.deepStrictEqual( - ForgejoCli.parseForgejoRemote("forgejo.local:maria/project.git"), - remote, - ); - assert.isUndefined(ForgejoCli.matchForgejoLogin(logins, remote!)); - assert.strictEqual( - ForgejoCli.matchForgejoLogin(logins, remote!, "forgejo.local:4000")?.name, - "two", - ); - assert.isUndefined(ForgejoCli.matchForgejoLogin(logins, remote!, "other.local:4000")); - const alias = ForgejoCli.parseForgejoRemote("git@ssh.forgejo.local:maria/project.git"); - assert.isNotNull(alias); - assert.isUndefined(ForgejoCli.matchForgejoLogin(logins, alias!, "forgejo.local:4000")); - const refined = ForgejoSourceControlProvider.discovery.refineUnknownRemote({ - cwd: "/repo", - context: { - provider: { kind: "unknown", name: "Forgejo", baseUrl: "https://forgejo.local" }, - remoteName: "origin", - remoteUrl: "git@forgejo.local:maria/project.git", - requestedHost: "forgejo.local:4000", - }, - auth: processOutput(yield* encodeJsonEffect(logins)), - }); - assert.strictEqual(refined?.baseUrl, "http://forgejo.local:4000"); - const https = ForgejoCli.parseForgejoRemote("http://forgejo.local:4000/maria/project.git"); - assert.isNotNull(https); - assert.strictEqual(ForgejoCli.matchForgejoLogin(logins, https!)?.name, "two"); - const hostOnly = ForgejoCli.parseForgejoRemote("http://forgejo.local:4000"); - assert.strictEqual( - ForgejoCli.matchForgejoLogin(logins, hostOnly!, undefined, true)?.name, - "two", - ); - const mounted = logins.map((login) => ({ - ...login, - url: `http://forgejo.local:4000/${login.name}`, - })); - assert.isUndefined(ForgejoCli.matchForgejoLogin(mounted, hostOnly!, undefined, true)); - }), -); - -it.effect("rejects HTTP failures even when tea exits successfully", () => - Effect.gen(function* () { - const cli = yield* ForgejoCli.make; - const result = yield* cli - .api({ - cwd: "/repo", - repository: "http://forgejo.local:3000/maria/project", - path: "repos/maria/project/pulls/42", - method: "PATCH", - body: { state: "closed" }, - }) - .pipe(Effect.result); - assert.strictEqual(result._tag, "Failure"); - if (result._tag === "Failure") - assert.strictEqual( - result.failure.detail, - "Forgejo repository or pull request was not found.", - ); - }).pipe( - Effect.provideService( - FileSystem.FileSystem, - FileSystem.makeNoop({ - exists: () => Effect.succeed(false), - }), - ), - Effect.provideService( - HttpClient.HttpClient, - HttpClient.make(() => { - throw new Error("tea must handle its own HTTP request"); - }), - ), - Effect.provide( - Layer.mock(VcsProcess.VcsProcess)({ - run: (input) => { - if (input.args[0] === "api") { - assert.strictEqual(input.stdin, '{"state":"closed"}'); - assert.include(input.args, "work"); - assert.include( - input.args, - "http://forgejo.local:3000/api/v1/repos/maria/project/pulls/42", - ); - } - return Effect.succeed( - input.args[0] === "login" - ? processOutput( - encodeJson([ - { - name: "work", - url: "http://forgejo.local:3000", - ssh_host: "forgejo.local", - user: "maria", - default: "true", - }, - ]), - ) - : processOutput('{"message":"not found"}', { stderr: "HTTP/1.1 404 Not Found\n" }), - ); - }, - }), - ), - ), -); - -it.effect("routes mounted Forgejo repositories without repeating the mount in API paths", () => - Effect.gen(function* () { - const cli = yield* ForgejoCli.make; - const viewer = yield* cli.api({ cwd: "/upstream-only", host: "code.test", path: "user" }); - assert.strictEqual(viewer.stdout, "[]"); - const mountedRepository = yield* cli.resolveRepository({ - cwd: "/upstream-only", - host: "code.test", - repository: "maria/project", - }); - assert.strictEqual(mountedRepository.baseUrl, "https://code.test/forgejo"); - assert.strictEqual(mountedRepository.repository, "maria/project"); - for (const path of [ - "repos/forgejo/maria/project/pulls?state=open", - "repos/forgejo/maria/project", - "repos/reviewer/project/contents/file.ts", - ]) { - const result = yield* cli.api({ - cwd: "/repo", - repository: "forgejo/maria/project", - context: { - provider: { kind: "forgejo", name: "Forgejo", baseUrl: "https://code.test/forgejo" }, - remoteName: "origin", - remoteUrl: "https://code.test/forgejo/maria/project.git", - }, - path, - }); - assert.strictEqual(result.stdout, "[]"); - } - const sameOwnerAsMount = yield* cli.resolveRepository({ - cwd: "/repo", - repository: "forgejo/project", - context: { - provider: { kind: "forgejo", name: "Forgejo", baseUrl: "https://code.test/forgejo" }, - remoteName: "origin", - remoteUrl: "ssh://git@code.test/forgejo/project.git", - }, - }); - assert.strictEqual(sameOwnerAsMount.command, "tea"); - assert.strictEqual(sameOwnerAsMount.repository, "forgejo/project"); - }).pipe( - Effect.provideService( - FileSystem.FileSystem, - FileSystem.makeNoop({ - exists: () => Effect.succeed(true), - readFileString: () => - Effect.succeed( - encodeJson({ - hosts: { "code.test/forgejo": { type: "Application", token: "test-token" } }, - }), - ), - }), - ), - Effect.provideService( - HttpClient.HttpClient, - HttpClient.make(() => { - throw new Error("tea must handle its own HTTP request"); - }), - ), - Effect.provide( - Layer.mock(VcsProcess.VcsProcess)({ - run: (input) => { - if (input.command === "git") - return Effect.succeed(processOutput("", { exitCode: ChildProcessSpawner.ExitCode(2) })); - if (input.args[0] === "login") - return Effect.succeed( - processOutput( - encodeJson([ - { - name: "mounted", - url: "https://code.test/forgejo", - ssh_host: "code.test", - user: "maria", - default: "true", - }, - ]), - ), - ); - const supported = [ - "https://code.test/forgejo/api/v1/user", - "https://code.test/forgejo/api/v1/repos/maria/project/pulls?state=open", - "https://code.test/forgejo/api/v1/repos/maria/project", - "https://code.test/forgejo/api/v1/repos/reviewer/project/contents/file.ts", - ]; - if (input.args.at(-1)?.endsWith("/user")) assert.notInclude(input.args, "--repo"); - assert.strictEqual(input.command, "tea"); - return Effect.succeed( - supported.includes(input.args.at(-1) ?? "") - ? processOutput("[]", { stderr: "HTTP/1.1 200 OK\n" }) - : processOutput("{}", { stderr: "HTTP/1.1 404 Not Found\n" }), - ); - }, - }), - ), - ), -); - -it.effect("prefers fj for HTTP and ported SSH aliases on root servers", () => { - const commands: string[] = []; - const requests: string[] = []; - return Effect.gen(function* () { - const cli = yield* ForgejoCli.make; - for (const remoteUrl of [ - "http://forgejo.local:3000/maria/project.git", - "ssh://git@ssh.forgejo.local:2222/maria/project.git", - "ssh://git@forgejo.local:2222/maria/project.git", - ]) { - const result = yield* cli.api({ - cwd: "/repo", - repository: "maria/project", - context: { - provider: { - kind: "forgejo", - name: "Forgejo", - baseUrl: "http://forgejo.local:3000", - }, - remoteName: "origin", - remoteUrl, - requestedHost: "forgejo.local:3000", - }, - path: "repos/maria/project/issues/42/comments", - method: "POST", - body: { body: "verified through fj" }, - }); - assert.strictEqual(result.stdout, '{"id":99}'); - } - assert.deepStrictEqual(commands, ["fj"]); - assert.deepStrictEqual(requests, [ - "http://forgejo.local:3000/api/v1/repos/maria/project/issues/42/comments", - "http://forgejo.local:3000/api/v1/repos/maria/project/issues/42/comments", - "http://forgejo.local:3000/api/v1/repos/maria/project/issues/42/comments", - ]); - const viewer = yield* cli.api({ - cwd: "/no-remotes", - host: "forgejo.local:3000", - path: "user", - }); - assert.strictEqual(viewer.stdout, '{"login":"maria"}'); - assert.strictEqual(requests.at(-1), "https://forgejo.local:3000/api/v1/user"); - const upstreamViewer = yield* cli.api({ - cwd: "/upstream-only", - host: "forgejo.local:3000", - path: "user", - }); - assert.strictEqual(upstreamViewer.stdout, '{"login":"maria"}'); - assert.strictEqual(requests.at(-1), "http://forgejo.local:3000/api/v1/user"); - const upstreamRepository = yield* cli.resolveRepository({ - cwd: "/upstream-only", - host: "forgejo.local:3000", - repository: "maria/project", - }); - assert.strictEqual(upstreamRepository.baseUrl, "http://forgejo.local:3000"); - assert.strictEqual(upstreamRepository.repository, "maria/project"); - const httpViewer = yield* cli.api({ cwd: "/repo", host: "forgejo.local:3000", path: "user" }); - assert.strictEqual(httpViewer.stdout, '{"login":"maria"}'); - assert.strictEqual(requests.at(-1), "http://forgejo.local:3000/api/v1/user"); - }).pipe( - Effect.provideService( - FileSystem.FileSystem, - FileSystem.makeNoop({ - exists: () => Effect.succeed(true), - readFileString: () => - Effect.succeed( - encodeJson({ - hosts: { - "forgejo.local:3000": { type: "Application", token: "test-token" }, - "forgejo.local:4000": { type: "Application", token: "other-token" }, - }, - aliases: { "ssh.forgejo.local:2222": "forgejo.local:3000" }, - }), - ), - }), - ), - Effect.provideService( - HttpClient.HttpClient, - HttpClient.make((request) => { - requests.push(request.url); - if (request.url.endsWith("/user")) { - assert.strictEqual(request.method, "GET"); - assert.strictEqual(request.headers.authorization, "token test-token"); - return Effect.succeed( - HttpClientResponse.fromWeb(request, new Response('{"login":"maria"}')), - ); - } - assert.strictEqual(request.method, "POST"); - assert.strictEqual(request.headers.authorization, "token test-token"); - assert.strictEqual(request.body._tag, "Uint8Array"); - if (request.body._tag === "Uint8Array") - assert.deepStrictEqual(JSON.parse(new TextDecoder().decode(request.body.body)), { - body: "verified through fj", - }); - return Effect.succeed( - HttpClientResponse.fromWeb(request, new Response('{"id":99}', { status: 201 })), - ); - }), - ), - Effect.provide( - Layer.mock(VcsProcess.VcsProcess)({ - run: (input) => { - commands.push(input.command); - if (input.command === "git") - return Effect.succeed( - input.cwd === "/no-remotes" - ? processOutput("", { exitCode: ChildProcessSpawner.ExitCode(2) }) - : processOutput( - `${input.cwd === "/upstream-only" ? "upstream" : "origin"}\thttp://forgejo.local:3000/maria/project.git (fetch)\nother\thttp://forgejo.local:3000/maria/other.git (fetch)\nunrelated\thttp://other.local:3000/maria/project.git (fetch)`, - ), - ); - assert.strictEqual(input.command, "fj"); - assert.deepStrictEqual(input.args, [ - "--host", - input.cwd === "/no-remotes" - ? "https://forgejo.local:3000" - : "http://forgejo.local:3000", - "whoami", - ]); - return Effect.succeed(processOutput("")); - }, - }), - ), - ); -}); - -it.effect("loads later fj review pages when the server caps pages below the requested size", () => { - const pages: number[] = []; - let issueCommentRequests = 0; - return Effect.gen(function* () { - const cli = yield* ForgejoCli.make; - const provider = yield* ForgejoPullRequestProvider.make.pipe( - Effect.provideService(ForgejoCli.ForgejoCli, cli), - ); - const activity = yield* provider.getChangeRequestActivity({ - cwd: "/repo", - repository: "maria/project", - host: "forgejo.test", - number: 42, - }); - assert.strictEqual(activity.commentCount, 42); - assert.strictEqual(activity.comments.at(-1)?.id, "review:41"); - assert.strictEqual(activity.commentsTruncated, false); - assert.deepStrictEqual(pages, [1, 2, 3]); - assert.strictEqual(issueCommentRequests, 1); - }).pipe( - Effect.provideService( - FileSystem.FileSystem, - FileSystem.makeNoop({ - exists: () => Effect.succeed(true), - readFileString: () => - Effect.succeed( - encodeJson({ hosts: { "forgejo.test": { type: "Application", token: "test-token" } } }), - ), - }), - ), - Effect.provideService( - HttpClient.HttpClient, - HttpClient.make((request) => { - const url = new URL(request.url); - if (url.pathname === "/api/v1/repos/maria/project/issues/42/comments") { - issueCommentRequests++; - return Effect.succeed( - HttpClientResponse.fromWeb( - request, - new Response( - encodeJson([ - { - id: 100, - body: "Unpaginated issue comment", - user: { login: "maria" }, - created_at: "2026-09-13T00:00:00Z", - }, - ]), - ), - ), - ); - } - if (url.pathname.endsWith("/reviews")) { - const page = Number(url.searchParams.get("page")); - pages.push(page); - assert.ok(page >= 1 && page <= 3); - const reviews = Array.from({ length: page < 3 ? 20 : 1 }, (_, index) => ({ - id: (page - 1) * 20 + index + 1, - body: "Review from a capped page", - user: { login: "maria" }, - state: "COMMENT", - submitted_at: "2026-09-13T00:00:00Z", - comments_count: 0, - })); - return Effect.succeed( - HttpClientResponse.fromWeb( - request, - new Response(encodeJson(reviews), { - headers: - page === 2 - ? {} - : { - Link: - page === 1 - ? `<${url.origin}${url.pathname}?limit=50&page=2>; rel="next"` - : `<${url.origin}${url.pathname}?limit=50&page=1>; rel="prev"`, - }, - }), - ), - ); - } - return Effect.succeed( - HttpClientResponse.fromWeb( - request, - new Response(encodeJson(url.pathname === "/api/v1/user" ? { login: "maria" } : [])), - ), - ); - }), - ), - Effect.provide( - Layer.mock(VcsProcess.VcsProcess)({ - run: (input) => { - if (input.command === "git") { - assert.deepStrictEqual(input.args, ["remote", "-v"]); - return Effect.succeed( - processOutput("origin\thttps://forgejo.test/maria/project.git (fetch)"), - ); - } - assert.strictEqual(input.command, "fj"); - assert.deepStrictEqual(input.args, ["--host", "https://forgejo.test", "whoami"]); - return Effect.succeed(processOutput("")); - }, - }), - ), - ); -}); - -it.effect("falls back to tea when fj is missing or has no account for this server", () => - Effect.gen(function* () { - for (const scenario of ["missing-cli", "missing-account", "stale-invalid-storage"] as const) { - const commands: string[] = []; - yield* Effect.gen(function* () { - const cli = yield* ForgejoCli.make; - const result = yield* cli.api({ - cwd: "/repo", - repository: "https://forgejo.local:3000/maria/project", - path: "repos/maria/project/pulls", - }); - assert.strictEqual(result.stdout, "[]"); - assert.deepStrictEqual( - commands, - scenario === "missing-account" ? ["tea", "tea"] : ["fj", "tea", "tea"], - ); - const viewer = yield* cli.api({ - cwd: "/upstream-only", - host: "forgejo.local:3000", - path: "user", - }); - assert.strictEqual(viewer.stdout, "[]"); - }).pipe( - Effect.provideService( - FileSystem.FileSystem, - FileSystem.makeNoop({ - exists: () => Effect.succeed(true), - readFileString: () => - Effect.succeed( - scenario === "stale-invalid-storage" - ? "invalid json" - : encodeJson({ - hosts: { - [scenario === "missing-cli" ? "forgejo.local:3000" : "other.local"]: { - type: "Application", - token: "test-token", - }, - }, - }), - ), - }), - ), - Effect.provideService( - HttpClient.HttpClient, - HttpClient.make(() => { - throw new Error("tea must handle its own HTTP request"); - }), - ), - Effect.provide( - Layer.mock(VcsProcess.VcsProcess)({ - run: (input) => { - commands.push(input.command); - if (input.command === "git") - return Effect.succeed( - processOutput("", { exitCode: ChildProcessSpawner.ExitCode(2) }), - ); - if (input.command === "fj") - return Effect.fail( - new VcsProcessSpawnError({ - operation: input.operation, - command: input.command, - cwd: input.cwd, - cause: new Error("fj not found"), - }), - ); - assert.strictEqual(input.command, "tea"); - if (input.args.at(-1)?.endsWith("/user")) assert.notInclude(input.args, "--repo"); - return Effect.succeed( - input.args[0] === "login" - ? processOutput( - encodeJson([ - { - name: "work", - url: "https://forgejo.local:3000", - user: "maria", - default: "true", - valid: "true", - }, - ]), - ) - : processOutput("[]", { stderr: "HTTP/1.1 200 OK\n" }), - ); - }, - }), - ), - ); - } - }), -); - -it.effect("handles fj mutation statuses without retrying failures or reading absent bodies", () => - Effect.gen(function* () { - for (const status of [204, 205, 302, 401, 403, 404, 429, 500]) { - let writes = 0; - yield* Effect.gen(function* () { - const cli = yield* ForgejoCli.make; - const result = yield* cli - .api({ - cwd: "/repo", - repository: "https://forgejo.local/maria/project", - path: "repos/maria/project/issues/42/comments", - method: "POST", - body: { body: "only once" }, - }) - .pipe(Effect.result); - assert.strictEqual(result._tag, status < 300 ? "Success" : "Failure"); - if (result._tag === "Success") assert.strictEqual(result.success.stdout, ""); - if (result._tag === "Failure") { - assert.strictEqual(result.failure.command, "fj"); - assert.strictEqual(result.failure.httpStatus, status); - } - assert.strictEqual(writes, 1); - }).pipe( - Effect.provideService( - FileSystem.FileSystem, - FileSystem.makeNoop({ - exists: () => Effect.succeed(true), - readFileString: () => - Effect.succeed( - encodeJson({ - hosts: { - "forgejo.local": { type: "Application", token: "test-token" }, - }, - }), - ), - }), - ), - Effect.provideService( - HttpClient.HttpClient, - HttpClient.make((request) => { - writes++; - assert.strictEqual( - request.url, - "https://forgejo.local/api/v1/repos/maria/project/issues/42/comments", - ); - return Effect.succeed( - HttpClientResponse.fromWeb( - request, - new Response(status < 300 ? null : "", { - status, - headers: { location: "https://other.local/" }, - }), - ), - ); - }), - ), - Effect.provide( - Layer.mock(VcsProcess.VcsProcess)({ - run: (input) => { - assert.strictEqual( - input.command, - "fj", - "a failed mutation must never switch accounts or CLI", - ); - return Effect.succeed(processOutput("")); - }, - }), - ), - ); - } - }), -); - -it.effect( - "discovers fj first and retains configured authentication failures instead of switching accounts", - () => - Effect.gen(function* () { - for (const scenario of ["authenticated", "revoked", "missing", "invalid-storage"] as const) { - const commands: string[] = []; - yield* Effect.gen(function* () { - const spec = yield* ForgejoSourceControlProvider.makeDiscovery; - assert.strictEqual(spec.type, "managed-cli"); - if (spec.type !== "managed-cli") return; - const result = yield* spec.probe("/repo"); - assert.strictEqual(result.executable, scenario === "missing" ? "tea" : "fj"); - assert.strictEqual( - result.auth.status, - scenario === "revoked" - ? "unauthenticated" - : scenario === "invalid-storage" - ? "unknown" - : "authenticated", - ); - if (scenario !== "revoked" && scenario !== "invalid-storage") - assert.deepStrictEqual(result.auth.host, Option.some("forgejo.local:3000")); - assert.deepStrictEqual( - result.auth.account, - scenario === "authenticated" || scenario === "missing" - ? Option.some("maria") - : Option.none(), - ); - assert.strictEqual( - commands.some((command) => command.startsWith("tea ")), - scenario === "missing", - ); - assert.include(commands, "fj version"); - }).pipe( - Effect.provide( - Layer.mergeAll( - Layer.mock(ForgejoCli.ForgejoCli)({ - getAccount: (input) => { - assert.strictEqual(scenario, "authenticated"); - assert.deepStrictEqual(input, { - cwd: "/repo", - baseUrl: "http://forgejo.local:3000", - }); - return Effect.succeed("maria"); - }, - listLogins: (input) => { - assert.strictEqual( - input.remoteUrl, - "http://forgejo.local:3000/maria/project.git", - ); - if (scenario === "invalid-storage") - return Effect.fail( - new ForgejoCli.ForgejoCliError({ - command: "fj", - cwd: input.cwd, - reason: "authentication", - detail: "fj authentication storage is invalid.", - }), - ); - return Effect.succeed([ - { - name: "forgejo.local:3000", - url: "http://forgejo.local:3000", - user: "", - default: "false", - }, - ]); - }, - }), - Layer.mock(VcsProcess.VcsProcess)({ - run: (input) => { - commands.push(`${input.command} ${input.args.join(" ")}`); - if (input.command === "git") - return Effect.succeed( - processOutput("http://forgejo.local:3000/maria/project.git\n"), - ); - if (input.command === "fj") { - if (scenario === "missing") - return Effect.fail( - new VcsProcessSpawnError({ - operation: input.operation, - command: input.command, - cwd: input.cwd, - cause: new Error("fj not found"), - }), - ); - if (input.args[0] === "version") - return Effect.succeed(processOutput("fj 0.10.0")); - if (scenario === "invalid-storage") { - assert.deepStrictEqual(input.args, ["auth", "list"]); - return Effect.succeed(processOutput("")); - } - assert.deepStrictEqual(input.args, [ - "--host", - "http://forgejo.local:3000", - "whoami", - ]); - return Effect.succeed( - processOutput("", { - exitCode: ChildProcessSpawner.ExitCode(scenario === "revoked" ? 1 : 0), - }), - ); - } - assert.strictEqual(input.command, "tea"); - return Effect.succeed( - input.args[0] === "--version" - ? processOutput("tea version 0.16.0") - : processOutput( - encodeJson([ - { - name: "work", - url: "http://forgejo.local:3000", - user: "maria", - default: "true", - valid: "true", - }, - ]), - ), - ); - }, - }), - ), - ), - ); - } - }), -); - it.effect( "checks out fj pull refs and preserves existing branches and dirty files until forced", () => @@ -1627,22 +385,26 @@ it.effect( args: ["rev-parse", "HEAD"], })).stdout.trim(); const fetched: string[] = []; + const host = yield* SourceControlHost.SourceControlHost; const provider = yield* ForgejoSourceControlProvider.make.pipe( Effect.provideService( - VcsProcess.VcsProcess, - VcsProcess.VcsProcess.of({ - run: (input) => { - if (input.args[0] !== "fetch") return git.run(input); - const url = input.args[2]; - assert.isDefined(url); - fetched.push(url!); - // Only SSH transport is substituted; both paths fetch the real pull ref. - return git.run({ - ...input, - args: input.args.map((arg) => - arg === "git@forgejo.test:reviewer/project.git" ? source : arg, - ), - }); + SourceControlHost.SourceControlHost, + SourceControlHost.SourceControlHost.of({ + settings: host.settings, + process: { + run: (input) => { + if (input.args[0] !== "fetch") return git.run(input); + const url = input.args[2]; + assert.isDefined(url); + fetched.push(url!); + // Only SSH transport is substituted; both paths fetch the real pull ref. + return git.run({ + ...input, + args: input.args.map((arg) => + arg === "git@forgejo.test:reviewer/project.git" ? source : arg, + ), + }); + }, }, }), ), @@ -1759,6 +521,12 @@ it.effect( assert.deepStrictEqual(fetched, [source, source, "git@forgejo.test:reviewer/project.git"]); }).pipe( Effect.scoped, - Effect.provide(VcsProcess.layer.pipe(Layer.provideMerge(NodeServices.layer))), + Effect.provide( + ServerSourceControlHost.layer.pipe( + Layer.provideMerge(VcsProcess.layer), + Layer.provideMerge(ServerSettings.ServerSettingsService.layerTest()), + Layer.provideMerge(NodeServices.layer), + ), + ), ), ); diff --git a/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts b/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts index 08a7259eb1e1..2e2969488d3d 100644 --- a/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts +++ b/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts @@ -19,7 +19,7 @@ import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as GitHubApi from "./GitHubApi.ts"; import * as GitHubCredentials from "./GitHubCredentials.ts"; import * as GitLabCli from "./GitLabCli.ts"; -import * as ForgejoCli from "./ForgejoCli.ts"; +import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; import * as ServerSourceControlHost from "./ServerSourceControlHost.ts"; diff --git a/apps/server/src/sourceControl/SourceControlProviderRegistry.ts b/apps/server/src/sourceControl/SourceControlProviderRegistry.ts index 309f79b1509f..22516853b836 100644 --- a/apps/server/src/sourceControl/SourceControlProviderRegistry.ts +++ b/apps/server/src/sourceControl/SourceControlProviderRegistry.ts @@ -15,7 +15,7 @@ import * as AzureDevOpsSourceControlProvider from "./AzureDevOpsSourceControlPro import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts"; -import * as ForgejoSourceControlProvider from "./ForgejoSourceControlProvider.ts"; +import * as BuiltInDrivers from "./builtInDrivers.ts"; import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; import { probeSourceControlProvider, @@ -308,11 +308,18 @@ export const make = Effect.gen(function* () { const github = yield* GitHubSourceControlProvider.make; const githubDiscovery = yield* GitHubSourceControlProvider.makeDiscovery; const gitlab = yield* GitLabSourceControlProvider.make; - const forgejo = yield* ForgejoSourceControlProvider.make; - const forgejoDiscovery = yield* ForgejoSourceControlProvider.makeDiscovery; const bitbucket = yield* BitbucketSourceControlProvider.make; const bitbucketDiscovery = yield* BitbucketSourceControlProvider.makeDiscovery; const azureDevOps = yield* AzureDevOpsSourceControlProvider.make; + const drivers = yield* Effect.forEach(BuiltInDrivers.BUILT_IN_SOURCE_CONTROL_DRIVERS, (driver) => + driver.make.pipe( + Effect.map((instance): SourceControlProviderRegistration => ({ + kind: driver.kind, + provider: instance.sourceControl, + discovery: instance.discovery, + })), + ), + ); return yield* makeWithProviders([ { kind: "github", @@ -334,7 +341,7 @@ export const make = Effect.gen(function* () { provider: bitbucket, discovery: bitbucketDiscovery, }, - { kind: "forgejo", provider: forgejo, discovery: forgejoDiscovery }, + ...drivers, ]); }); diff --git a/apps/server/src/sourceControl/builtInDrivers.ts b/apps/server/src/sourceControl/builtInDrivers.ts new file mode 100644 index 000000000000..c49319ba0a1f --- /dev/null +++ b/apps/server/src/sourceControl/builtInDrivers.ts @@ -0,0 +1,20 @@ +/** + * The source control drivers this build ships with. Both registries, repository operations and + * pull requests, iterate this list; a host with no driver here shows up as unsupported. + * + * Adding a host means writing its `@t3tools/source-control-` package, adding its driver + * here, and providing its services' layers in `layer` below. + * + * @module sourceControl/builtInDrivers + */ +import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; +import * as ForgejoDriver from "@t3tools/source-control-forgejo/server/driver"; +import * as Layer from "effect/Layer"; + +import * as ServerSourceControlHost from "./ServerSourceControlHost.ts"; + +/** Ordered as the hosts appear in discovery, after the ones still built in the server. */ +export const BUILT_IN_SOURCE_CONTROL_DRIVERS = [ForgejoDriver.driver] as const; + +/** The services the built-in drivers' packages own, plus the host port they all run against. */ +export const layer = ForgejoCli.layer.pipe(Layer.provideMerge(ServerSourceControlHost.layer)); diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 666805e1d79e..d3e27ea0bc81 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -233,9 +233,8 @@ import * as AzureDevOpsCli from "./sourceControl/AzureDevOpsCli.ts"; import * as BitbucketApi from "./sourceControl/BitbucketApi.ts"; import * as GitHubApi from "./sourceControl/GitHubApi.ts"; import * as GitLabCli from "./sourceControl/GitLabCli.ts"; -import * as ForgejoCli from "./sourceControl/ForgejoCli.ts"; +import * as SourceControlBuiltInDrivers from "./sourceControl/builtInDrivers.ts"; import * as SourceControlProviderRegistry from "./sourceControl/SourceControlProviderRegistry.ts"; -import * as ServerSourceControlHost from "./sourceControl/ServerSourceControlHost.ts"; import * as GitVcsDriver from "./vcs/GitVcsDriver.ts"; import * as VcsDriverRegistry from "./vcs/VcsDriverRegistry.ts"; import * as VcsProjectConfig from "./vcs/VcsProjectConfig.ts"; @@ -3209,8 +3208,7 @@ export const layer = Layer.unwrap( BitbucketApi.layer, GitHubApi.layerWithDependencies, GitLabCli.layer, - ForgejoCli.layer, - ServerSourceControlHost.layer, + SourceControlBuiltInDrivers.layer, ), ), Layer.provideMerge(GitVcsDriver.layer), diff --git a/package.json b/package.json index 471e5361eef2..ed9f936dfbf9 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "tc": "vp run -r --concurrency-limit 2 typecheck", "lint": "vp lint --report-unused-disable-directives", "knip": "knip --preprocessor ./scripts/knip-schemas.ts", - "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-acp --workspace packages/provider-acp-registry --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --workspace packages/provider-cursor --workspace packages/provider-grok --workspace packages/source-control-core --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", + "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-acp --workspace packages/provider-acp-registry --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --workspace packages/provider-cursor --workspace packages/provider-grok --workspace packages/source-control-core --workspace packages/source-control-forgejo --workspace packages/source-control-testing --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", "knip:production": "knip --production --preprocessor ./scripts/knip-schemas.ts", "lint:mobile": "node scripts/mobile-native-static-check.ts", "test": "vp run -r test", diff --git a/packages/source-control-core/package.json b/packages/source-control-core/package.json index 58742f9f3589..ee1045b278cd 100644 --- a/packages/source-control-core/package.json +++ b/packages/source-control-core/package.json @@ -3,10 +3,18 @@ "private": true, "type": "module", "exports": { + "./server/diffFileRevisions": { + "types": "./src/server/diffFileRevisions.ts", + "import": "./src/server/diffFileRevisions.ts" + }, "./server/discovery": { "types": "./src/server/discovery.ts", "import": "./src/server/discovery.ts" }, + "./server/driver": { + "types": "./src/server/driver.ts", + "import": "./src/server/driver.ts" + }, "./server/mergeMessage": { "types": "./src/server/mergeMessage.ts", "import": "./src/server/mergeMessage.ts" @@ -38,6 +46,7 @@ }, "dependencies": { "@t3tools/contracts": "workspace:*", + "@t3tools/shared": "workspace:*", "effect": "catalog:" }, "devDependencies": { diff --git a/apps/server/src/pullRequest/bitbucketDiffRevisions.test.ts b/packages/source-control-core/src/server/diffFileRevisions.test.ts similarity index 99% rename from apps/server/src/pullRequest/bitbucketDiffRevisions.test.ts rename to packages/source-control-core/src/server/diffFileRevisions.test.ts index 1426a67d29da..cd1790adf8bb 100644 --- a/apps/server/src/pullRequest/bitbucketDiffRevisions.test.ts +++ b/packages/source-control-core/src/server/diffFileRevisions.test.ts @@ -1,6 +1,6 @@ import { assert, describe, it } from "@effect/vitest"; -import { parseDiffFileRevisions } from "./bitbucketDiffRevisions.ts"; +import { parseDiffFileRevisions } from "./diffFileRevisions.ts"; function patchOf(...lines: ReadonlyArray): string { return `${lines.join("\n")}\n`; diff --git a/apps/server/src/pullRequest/bitbucketDiffRevisions.ts b/packages/source-control-core/src/server/diffFileRevisions.ts similarity index 100% rename from apps/server/src/pullRequest/bitbucketDiffRevisions.ts rename to packages/source-control-core/src/server/diffFileRevisions.ts diff --git a/packages/source-control-core/src/server/driver.ts b/packages/source-control-core/src/server/driver.ts new file mode 100644 index 000000000000..3a227d0928d8 --- /dev/null +++ b/packages/source-control-core/src/server/driver.ts @@ -0,0 +1,36 @@ +/** + * SourceControlDriver — what a source control provider package hands the server. + * + * One driver per host kind. The server lists its drivers once and both registries (repository + * operations and pull requests) iterate that list, so adding a host means writing its package + * and adding its driver to the list. + * + * `make` yields what it needs from the environment: the `SourceControlHost` port, Effect + * platform services, and the package's own services, which the server provides from the + * package's layers. Its `R` channel is the full list. + * + * @module source-control-core/server/driver + */ +import type { SourceControlProviderKind } from "@t3tools/contracts"; +import type * as Effect from "effect/Effect"; + +import type { SourceControlProviderDiscoverySpec } from "./discovery.ts"; +import type { PullRequestProviderApi } from "./PullRequestProvider.ts"; +import type * as SourceControlProvider from "./SourceControlProvider.ts"; + +export interface SourceControlDriverInstance { + readonly sourceControl: SourceControlProvider.SourceControlProvider["Service"]; + readonly discovery: SourceControlProviderDiscoverySpec; + /** Null for a host whose pull requests this build cannot read. */ + readonly pullRequests: PullRequestProviderApi | null; +} + +export interface SourceControlDriver { + readonly kind: SourceControlProviderKind; + readonly make: Effect.Effect; +} + +/** Declares a driver with its `R` inferred from `make`. */ +export const defineSourceControlDriver = ( + driver: SourceControlDriver, +): SourceControlDriver => driver; diff --git a/packages/source-control-forgejo/package.json b/packages/source-control-forgejo/package.json new file mode 100644 index 000000000000..4c6b534e2e81 --- /dev/null +++ b/packages/source-control-forgejo/package.json @@ -0,0 +1,48 @@ +{ + "name": "@t3tools/source-control-forgejo", + "private": true, + "type": "module", + "exports": { + "./server/driver": { + "types": "./src/server/driver.ts", + "import": "./src/server/driver.ts" + }, + "./server/ForgejoCli": { + "types": "./src/server/ForgejoCli.ts", + "import": "./src/server/ForgejoCli.ts" + }, + "./server/forgejoPullRequestJson": { + "types": "./src/server/forgejoPullRequestJson.ts", + "import": "./src/server/forgejoPullRequestJson.ts" + }, + "./server/ForgejoPullRequestProvider": { + "types": "./src/server/ForgejoPullRequestProvider.ts", + "import": "./src/server/ForgejoPullRequestProvider.ts" + }, + "./server/forgejoPullRequests": { + "types": "./src/server/forgejoPullRequests.ts", + "import": "./src/server/forgejoPullRequests.ts" + }, + "./server/ForgejoSourceControlProvider": { + "types": "./src/server/ForgejoSourceControlProvider.ts", + "import": "./src/server/ForgejoSourceControlProvider.ts" + } + }, + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vp test run --config ../../vite.config.ts --dir ." + }, + "dependencies": { + "@t3tools/contracts": "workspace:*", + "@t3tools/provider-core": "workspace:*", + "@t3tools/shared": "workspace:*", + "@t3tools/source-control-core": "workspace:*", + "effect": "catalog:" + }, + "devDependencies": { + "@effect/vitest": "catalog:", + "@t3tools/source-control-testing": "workspace:*", + "@types/node": "catalog:", + "vite-plus": "catalog:" + } +} diff --git a/apps/server/src/sourceControl/ForgejoCli.ts b/packages/source-control-forgejo/src/server/ForgejoCli.ts similarity index 98% rename from apps/server/src/sourceControl/ForgejoCli.ts rename to packages/source-control-forgejo/src/server/ForgejoCli.ts index 573c081e5a42..73b0098aa403 100644 --- a/apps/server/src/sourceControl/ForgejoCli.ts +++ b/packages/source-control-forgejo/src/server/ForgejoCli.ts @@ -13,7 +13,7 @@ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http"; import { ChildProcessSpawner } from "effect/process"; import { decodeJsonResult } from "@t3tools/shared/schemaJson"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; -import * as VcsProcess from "../vcs/VcsProcess.ts"; +import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; import { collectUint8StreamText } from "@t3tools/provider-core/server/collectStreamText"; import type { SourceControlProviderContext } from "@t3tools/source-control-core/server/SourceControlProvider"; @@ -133,7 +133,7 @@ export class ForgejoCli extends Context.Service< readonly stdin?: string; readonly timeoutMs?: number; readonly maxOutputBytes?: number; - }) => Effect.Effect; + }) => Effect.Effect; readonly listLogins?: (input: { readonly cwd: string; readonly command: "fj" | "tea"; @@ -148,9 +148,9 @@ export class ForgejoCli extends Context.Service< ) => Effect.Effect; readonly api: ( input: ForgejoApiInput, - ) => Effect.Effect; + ) => Effect.Effect; } ->()("t3/sourceControl/ForgejoCli") {} +>()("@t3tools/source-control-forgejo/server/ForgejoCli") {} export function parseForgejoRemote(value: string) { if (/^(?:https?|ssh):\/\//i.test(value)) { @@ -212,7 +212,7 @@ export function matchForgejoLogin( } export const make = Effect.gen(function* () { - const process = yield* VcsProcess.VcsProcess; + const { process } = yield* SourceControlHost.SourceControlHost; const fileSystem = yield* FileSystem.FileSystem; const httpClient = yield* HttpClient.HttpClient; const authLock = yield* Semaphore.make(1); diff --git a/apps/server/src/pullRequest/ForgejoPullRequestProvider.ts b/packages/source-control-forgejo/src/server/ForgejoPullRequestProvider.ts similarity index 99% rename from apps/server/src/pullRequest/ForgejoPullRequestProvider.ts rename to packages/source-control-forgejo/src/server/ForgejoPullRequestProvider.ts index a9337fdbe8fd..4a2d8afba121 100644 --- a/apps/server/src/pullRequest/ForgejoPullRequestProvider.ts +++ b/packages/source-control-forgejo/src/server/ForgejoPullRequestProvider.ts @@ -3,8 +3,8 @@ import * as Schema from "effect/Schema"; import * as Result from "effect/Result"; import type { PullRequestCapabilities, PullRequestViewerPermissions } from "@t3tools/contracts"; import { decodeJsonResult } from "@t3tools/shared/schemaJson"; -import * as ForgejoCli from "../sourceControl/ForgejoCli.ts"; -import { parseDiffFileRevisions } from "./bitbucketDiffRevisions.ts"; +import * as ForgejoCli from "./ForgejoCli.ts"; +import { parseDiffFileRevisions } from "@t3tools/source-control-core/server/diffFileRevisions"; import { PullRequestProviderError, type ProviderChangeRequestDetail, diff --git a/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts b/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts new file mode 100644 index 000000000000..679a4e69028c --- /dev/null +++ b/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts @@ -0,0 +1,1283 @@ +import { assert, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import type * as Context from "effect/Context"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; +import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; +import * as FetchHttpClient from "effect/http/FetchHttpClient"; +import * as HttpClient from "effect/http/HttpClient"; +import * as HttpClientResponse from "effect/http/HttpClientResponse"; +import { VcsProcessSpawnError } from "@t3tools/contracts"; +import { firstNonEmptyLine } from "@t3tools/source-control-core/server/discovery"; +import * as TestSourceControlHost from "@t3tools/source-control-testing/TestSourceControlHost"; + +import * as ForgejoCli from "./ForgejoCli.ts"; +import * as ForgejoPullRequestProvider from "./ForgejoPullRequestProvider.ts"; +import * as ForgejoSourceControlProvider from "./ForgejoSourceControlProvider.ts"; + +const processOutput = TestSourceControlHost.processOutput; + +const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); +const encodeJsonEffect = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); + +it.effect("submits a Forgejo review without sending its summary in the preliminary GET", () => { + const methods: string[] = []; + const fetchReview = async ( + ...[input, init]: Parameters> + ) => { + const request = new Request(input instanceof Request ? input.url : String(input), { + ...(init?.method === undefined ? {} : { method: init.method }), + ...(init?.headers === undefined ? {} : { headers: init.headers }), + ...(init?.body === undefined ? {} : { body: init.body }), + }); + methods.push(request.method); + if (request.method === "GET") { + assert.strictEqual(request.url, "https://forgejo.test/api/v1/repos/maria/project/pulls/42"); + return new Response( + encodeJson({ + number: 42, + title: "Review target", + body: "", + html_url: "https://forgejo.test/maria/project/pulls/42", + user: { login: "maria" }, + state: "open", + merged: false, + head: { ref: "feature", sha: "head", repo: null }, + base: { ref: "main", sha: "base", repo: null }, + created_at: "2026-09-13T00:00:00Z", + updated_at: "2026-09-13T00:00:00Z", + closed_at: null, + merged_at: null, + labels: [], + }), + ); + } + assert.strictEqual(request.method, "POST"); + assert.strictEqual( + request.url, + "https://forgejo.test/api/v1/repos/maria/project/pulls/42/reviews", + ); + assert.deepStrictEqual(JSON.parse(await request.text()), { + event: "COMMENT", + body: "Review summary", + commit_id: "head", + comments: [], + }); + return new Response('{"id":1}', { status: 200 }); + }; + return Effect.gen(function* () { + const cli = yield* ForgejoCli.make; + const provider = yield* ForgejoPullRequestProvider.make.pipe( + Effect.provideService(ForgejoCli.ForgejoCli, cli), + ); + yield* provider.submitReview({ + cwd: "/repo", + repository: "maria/project", + host: "forgejo.test", + number: 42, + verdict: "comment", + body: "Review summary", + comments: [], + }); + assert.deepStrictEqual(methods, ["GET", "POST"]); + }).pipe( + Effect.provideService( + FetchHttpClient.Fetch, + Object.assign(fetchReview, { preconnect: () => undefined }), + ), + Effect.provide(FetchHttpClient.layer), + Effect.provideService( + FileSystem.FileSystem, + FileSystem.makeNoop({ + exists: () => Effect.succeed(true), + readFileString: () => + Effect.succeed( + encodeJson({ hosts: { "forgejo.test": { type: "Application", token: "test-token" } } }), + ), + }), + ), + Effect.provide( + TestSourceControlHost.layer({ + process: { + run: (input) => { + if (input.command === "git") { + assert.deepStrictEqual(input.args, ["remote", "-v"]); + return Effect.succeed( + processOutput("origin\thttps://forgejo.test/maria/project.git (fetch)"), + ); + } + assert.strictEqual(input.command, "fj"); + assert.deepStrictEqual(input.args, ["--host", "https://forgejo.test", "whoami"]); + return Effect.succeed(processOutput("")); + }, + }, + }), + ), + ); +}); + +it.effect("reads Forgejo checks without repository or viewer requests", () => { + const paths: string[] = []; + return Effect.gen(function* () { + const provider = yield* ForgejoPullRequestProvider.make; + const read = provider.getChangeRequestChecks; + if (read === undefined) return yield* Effect.die("checks read missing"); + const result = yield* read({ + cwd: "/repo", + repository: "acme/web", + host: "forgejo.test", + number: 1, + }); + assert.strictEqual(result.state, "open"); + assert.strictEqual(result.checks[0]?.status, "failure"); + assert.deepStrictEqual(paths, [ + "repos/acme/web/pulls/1", + "repos/acme/web/statuses/head?sort=recentupdate&limit=50&page=1", + "repos/acme/web/statuses/head?sort=recentupdate&limit=50&page=2", + ]); + }).pipe( + Effect.provide( + Layer.mock(ForgejoCli.ForgejoCli)({ + api: (input) => { + paths.push(input.path); + assert.match(input.path, /^repos\/acme\/web\/(pulls\/1|statuses\/head)/); + return Effect.succeed( + processOutput( + input.path.endsWith("pulls/1") + ? `{"number":1,"title":"Checks","body":"","html_url":"https://forgejo.test/acme/web/pulls/1", "user":null,"state":"open","merged":false, + "head":{"ref":"feature","sha":"head","repo":null},"base":{"ref":"main","sha":"base","repo":null}, + "created_at":"2026-09-16T00:00:00Z","updated_at":"2026-09-16T00:00:00Z","closed_at":null,"merged_at":null,"labels":[]}` + : input.path.endsWith("page=1") + ? `[{"context":"build","status":"failure","description":null,"target_url":null,"updated_at":"2026-09-16T00:00:00Z"}]` + : "[]", + ), + ); + }, + }), + ), + ); +}); + +it.effect.each([ + ["Ready", false, true, false, "mergeable"], + ["Draft", true, false, true, "unknown"], + ["Blocked", false, false, false, "unknown"], + ["Unchecked", false, undefined, false, "unknown"], + ["WIP: Legacy draft", undefined, false, true, "unknown"], + ["[WIP] Legacy draft", undefined, false, true, "unknown"], + ["WIP: Explicitly ready", false, false, false, "unknown"], +] as const)( + "reads Forgejo mergeability for %s (draft=%s, mergeable=%s) across list, summary and detail", + ([title, draft, mergeable, isDraft, mergeability]) => { + const pr = { + number: 42, + title, + body: "", + html_url: "https://forgejo.test/maria/project/pulls/42", + user: { login: "maria" }, + state: "open", + merged: false, + ...(draft === undefined ? {} : { draft }), + ...(mergeable === undefined ? {} : { mergeable }), + head: { ref: "feature", sha: "head", repo: null }, + base: { ref: "main", sha: "base", repo: null }, + created_at: "2026-09-16T00:00:00Z", + updated_at: "2026-09-16T00:00:00Z", + closed_at: null, + merged_at: null, + labels: [], + }; + return Effect.gen(function* () { + const provider = yield* ForgejoPullRequestProvider.make; + const readSummary = provider.getChangeRequestSummary; + if (readSummary === undefined) return yield* Effect.die("summary read missing"); + const input = { cwd: "/repo", repository: "maria/project", host: "forgejo.test", number: 42 }; + const list = yield* provider.listChangeRequests({ + ...input, + state: "open", + involvement: "all", + viewer: "maria", + limit: 10, + }); + const summary = yield* readSummary(input); + const detail = yield* provider.getChangeRequest(input); + assert.strictEqual(list.items.length, 1); + for (const result of [list.items[0]!, summary, detail]) { + assert.strictEqual(result.mergeability, mergeability); + assert.strictEqual(result.isDraft, isDraft); + } + }).pipe( + Effect.provide( + Layer.mock(ForgejoCli.ForgejoCli)({ + api: (input) => { + const [path, query] = input.path.split("?"); + let response: unknown; + switch (path) { + case "repos/maria/project/pulls": + response = new URLSearchParams(query).get("page") === "1" ? [pr] : []; + break; + case "repos/maria/project/pulls/42": + response = pr; + break; + case "repos/maria/project": + response = { full_name: "maria/project", permissions: { push: true, admin: true } }; + break; + case "user": + response = pr.user; + break; + case "repos/maria/project/statuses/head": + response = []; + break; + default: + return Effect.die(`Unexpected Forgejo request: ${input.path}`); + } + return encodeJsonEffect(response).pipe(Effect.orDie, Effect.map(processOutput)); + }, + }), + ), + ); + }, +); + +it.effect("loads Forgejo pull request references from files and commits views", () => + Effect.gen(function* () { + const provider = yield* ForgejoSourceControlProvider.make; + for (const reference of [ + "42", + "#42", + "https://forgejo.test/maria/project/pulls/42", + "https://forgejo.test/maria/project/pulls/42/", + "https://forgejo.test/maria/project/pulls/42/files?w=1#diff-1", + "http://forgejo.test:3000/git/maria/project/pulls/42/commits", + ]) { + const result = yield* provider.getChangeRequest({ cwd: "/repo", reference }); + assert.strictEqual(result.number, 42); + assert.strictEqual(result.title, "Forgejo view reference"); + } + const invalid = yield* provider + .getChangeRequest({ + cwd: "/repo", + reference: "https://forgejo.test/maria/project/pulls/42invalid/files", + }) + .pipe(Effect.result); + assert.strictEqual(invalid._tag, "Failure"); + }).pipe( + Effect.provide( + Layer.mergeAll( + Layer.succeed(FileSystem.FileSystem, FileSystem.makeNoop({})), + TestSourceControlHost.layer({ process: {} }), + Layer.mock(ForgejoCli.ForgejoCli)({ + resolveRepository: () => + Effect.succeed({ + login: "work", + repository: "maria/project", + baseUrl: "https://forgejo.test", + }), + api: (input) => { + assert.strictEqual(input.path, "repos/maria/project/pulls/42"); + return encodeJsonEffect({ + number: 42, + title: "Forgejo view reference", + html_url: "https://forgejo.test/maria/project/pulls/42", + state: "open", + merged: false, + base: { ref: "main", sha: "base", repo: null }, + head: { ref: "feature", sha: "head", repo: null }, + }).pipe(Effect.orDie, Effect.map(processOutput)); + }, + }), + ), + ), + ), +); + +it.effect( + "loads Forgejo reactions on comments, reviews and inline threads and resolves review mutations", + () => { + const user = { login: "maria" }; + const review = { + id: 8, + body: "review body", + user, + state: "COMMENT", + submitted_at: "2026-09-12T00:00:00Z", + html_url: "https://forgejo.test/maria/project/pulls/2#issuecomment-37", + comments_count: 1, + }; + const comment = { id: 12, body: "ordinary", user, created_at: review.submitted_at }; + const responses: Record = { + user, + "repos/maria/project/issues/2/comments": [comment], + "repos/maria/project/pulls/2/reviews": [review], + "repos/maria/project/pulls/2/reviews/8": review, + "repos/maria/project/pulls/2/reviews/9": { ...review, id: 9, html_url: "" }, + "repos/maria/project/pulls/2/commits": [], + "repos/maria/project/issues/2/reactions": [], + "repos/maria/project/pulls/2/reviews/8/comments": [ + { + ...comment, + id: 38, + body: "inline", + path: "file.ts", + position: 1, + original_position: 1, + commit_id: "head", + original_commit_id: "head", + resolver: null, + }, + ], + "repos/maria/project/issues/comments/12/reactions": [{ content: "+1", user }], + "repos/maria/project/issues/comments/37/reactions": [{ content: "heart", user }], + "repos/maria/project/issues/comments/38/reactions": [ + { content: "rocket", user: { login: "reviewer" } }, + ], + }; + const writes: ForgejoCli.ForgejoApiInput[] = []; + let reactionReads = 0; + return Effect.gen(function* () { + const provider = yield* ForgejoPullRequestProvider.make; + const input = { cwd: "/repo", repository: "maria/project", host: "forgejo.test", number: 2 }; + const activity = yield* provider.getChangeRequestActivity(input); + assert.deepStrictEqual( + activity.comments.map((entry) => ({ + id: entry.id, + kind: entry.kind, + reactions: entry.reactions, + })), + [ + { + id: "12", + kind: "issue-comment", + reactions: [{ content: "thumbs-up", count: 1, actors: [], viewerHasReacted: true }], + }, + { + id: "review:8", + kind: "review", + reactions: [{ content: "heart", count: 1, actors: [], viewerHasReacted: true }], + }, + { + id: "38", + kind: "review-comment", + reactions: [ + { content: "rocket", count: 1, actors: ["reviewer"], viewerHasReacted: false }, + ], + }, + ], + ); + const inlineComment = activity.comments[2]; + assert.ok(inlineComment); + assert.deepStrictEqual(activity.reviewThreads[0]?.comments, [inlineComment]); + for (const reacted of [true, false]) { + yield* provider.setReaction({ ...input, subjectId: "review:8", content: "heart", reacted }); + yield* provider.setReaction({ ...input, subjectId: "38", content: "rocket", reacted }); + } + assert.deepStrictEqual( + writes.map(({ path, method, body }) => ({ path, method, body })), + [ + { + path: "repos/maria/project/issues/comments/37/reactions", + method: "POST", + body: { content: "heart" }, + }, + { + path: "repos/maria/project/issues/comments/38/reactions", + method: "POST", + body: { content: "rocket" }, + }, + { + path: "repos/maria/project/issues/comments/37/reactions", + method: "DELETE", + body: { content: "heart" }, + }, + { + path: "repos/maria/project/issues/comments/38/reactions", + method: "DELETE", + body: { content: "rocket" }, + }, + ], + ); + const missing = yield* provider + .setReaction({ ...input, subjectId: "review:9", content: "heart", reacted: true }) + .pipe(Effect.result); + assert.strictEqual(missing._tag, "Failure"); + if (missing._tag === "Failure") assert.include(missing.failure.detail, "comment ID"); + assert.strictEqual(writes.length, 4); + responses["repos/maria/project/pulls/2/reviews/8/comments"] = Array.from( + { length: 501 }, + (_, index) => ({ + ...comment, + id: 1000 + index, + path: "file.ts", + position: 1, + original_position: 1, + commit_id: "head", + original_commit_id: "head", + resolver: null, + }), + ); + for (let index = 0; index < 500; index++) { + responses[`repos/maria/project/issues/comments/${1000 + index}/reactions`] = []; + } + reactionReads = 0; + const bounded = yield* provider.getChangeRequestActivity(input); + assert.strictEqual(bounded.reviewThreads.length, 500); + assert.strictEqual( + bounded.comments.filter((entry) => entry.kind === "review-comment").length, + 500, + ); + assert.strictEqual(bounded.commentsTruncated, true); + assert.strictEqual(reactionReads, 502); + }).pipe( + Effect.provide( + Layer.mock(ForgejoCli.ForgejoCli)({ + api: (input) => { + if (input.method) { + writes.push(input); + return Effect.succeed(processOutput("{}")); + } + const path = input.path.split("?")[0]!; + if (/\/issues\/comments\/\d+\/reactions$/.test(path)) reactionReads++; + assert.ok(Object.hasOwn(responses, path), `Unexpected Forgejo request: ${path}`); + const page = Number(new URLSearchParams(input.path.split("?")[1]).get("page")); + return encodeJsonEffect(page > 1 ? [] : responses[path]).pipe( + Effect.orDie, + Effect.map(processOutput), + ); + }, + }), + ), + ); + }, +); + +it.effect("discovers Forgejo accounts and retains the server port", () => + Effect.gen(function* () { + const auth = ForgejoSourceControlProvider.discovery.parseAuth( + processOutput( + yield* encodeJsonEffect([ + { + name: "work", + url: "http://forgejo.local:3000", + ssh_host: "git.forgejo.local", + user: "maria", + default: "true", + valid: "true", + }, + ]), + ), + ); + assert.deepStrictEqual( + firstNonEmptyLine("\u001b[1mtea version 0.16.0\u001b[0m\n"), + Option.some("tea version 0.16.0"), + ); + assert.strictEqual(auth.status, "authenticated"); + assert.deepStrictEqual(auth.account, Option.some("maria")); + assert.deepStrictEqual(auth.host, Option.some("forgejo.local:3000")); + const revoked = ForgejoSourceControlProvider.discovery.parseAuth( + processOutput( + encodeJson([ + { + name: "work", + url: "http://forgejo.local:3000", + user: "maria", + default: "true", + valid: "false", + }, + ]), + ), + ); + assert.strictEqual(revoked.status, "unauthenticated"); + const refined = ForgejoSourceControlProvider.discovery.refineUnknownRemote({ + cwd: "/repo", + context: { + provider: { + kind: "unknown", + name: "git.forgejo.local", + baseUrl: "https://git.forgejo.local", + }, + remoteName: "origin", + remoteUrl: "git@git.forgejo.local:maria/project.git", + }, + auth: processOutput( + yield* encodeJsonEffect([ + { + name: "work", + url: "http://forgejo.local:3000", + ssh_host: "git.forgejo.local", + user: "maria", + default: "true", + }, + ]), + ), + }); + assert.deepStrictEqual(refined, { + kind: "forgejo", + name: "Forgejo / Gitea", + baseUrl: "http://forgejo.local:3000", + }); + }), +); + +it.effect("does not choose a default Forgejo login across ambiguous SSH server ports", () => + Effect.gen(function* () { + const logins = ForgejoCli.parseForgejoLogins( + yield* encodeJsonEffect([ + { + name: "one", + url: "http://forgejo.local:3000", + ssh_host: "forgejo.local", + user: "maria", + default: "true", + }, + { + name: "two", + url: "http://forgejo.local:4000", + ssh_host: "forgejo.local", + user: "maria", + default: "false", + }, + ]), + ); + const remote = ForgejoCli.parseForgejoRemote("git@forgejo.local:maria/project.git"); + assert.isNotNull(remote); + assert.deepStrictEqual( + ForgejoCli.parseForgejoRemote("forgejo.local:maria/project.git"), + remote, + ); + assert.isUndefined(ForgejoCli.matchForgejoLogin(logins, remote!)); + assert.strictEqual( + ForgejoCli.matchForgejoLogin(logins, remote!, "forgejo.local:4000")?.name, + "two", + ); + assert.isUndefined(ForgejoCli.matchForgejoLogin(logins, remote!, "other.local:4000")); + const alias = ForgejoCli.parseForgejoRemote("git@ssh.forgejo.local:maria/project.git"); + assert.isNotNull(alias); + assert.isUndefined(ForgejoCli.matchForgejoLogin(logins, alias!, "forgejo.local:4000")); + const refined = ForgejoSourceControlProvider.discovery.refineUnknownRemote({ + cwd: "/repo", + context: { + provider: { kind: "unknown", name: "Forgejo", baseUrl: "https://forgejo.local" }, + remoteName: "origin", + remoteUrl: "git@forgejo.local:maria/project.git", + requestedHost: "forgejo.local:4000", + }, + auth: processOutput(yield* encodeJsonEffect(logins)), + }); + assert.strictEqual(refined?.baseUrl, "http://forgejo.local:4000"); + const https = ForgejoCli.parseForgejoRemote("http://forgejo.local:4000/maria/project.git"); + assert.isNotNull(https); + assert.strictEqual(ForgejoCli.matchForgejoLogin(logins, https!)?.name, "two"); + const hostOnly = ForgejoCli.parseForgejoRemote("http://forgejo.local:4000"); + assert.strictEqual( + ForgejoCli.matchForgejoLogin(logins, hostOnly!, undefined, true)?.name, + "two", + ); + const mounted = logins.map((login) => ({ + ...login, + url: `http://forgejo.local:4000/${login.name}`, + })); + assert.isUndefined(ForgejoCli.matchForgejoLogin(mounted, hostOnly!, undefined, true)); + }), +); + +it.effect("rejects HTTP failures even when tea exits successfully", () => + Effect.gen(function* () { + const cli = yield* ForgejoCli.make; + const result = yield* cli + .api({ + cwd: "/repo", + repository: "http://forgejo.local:3000/maria/project", + path: "repos/maria/project/pulls/42", + method: "PATCH", + body: { state: "closed" }, + }) + .pipe(Effect.result); + assert.strictEqual(result._tag, "Failure"); + if (result._tag === "Failure") + assert.strictEqual( + result.failure.detail, + "Forgejo repository or pull request was not found.", + ); + }).pipe( + Effect.provideService( + FileSystem.FileSystem, + FileSystem.makeNoop({ + exists: () => Effect.succeed(false), + }), + ), + Effect.provideService( + HttpClient.HttpClient, + HttpClient.make(() => { + throw new Error("tea must handle its own HTTP request"); + }), + ), + Effect.provide( + TestSourceControlHost.layer({ + process: { + run: (input) => { + if (input.args[0] === "api") { + assert.strictEqual(input.stdin, '{"state":"closed"}'); + assert.include(input.args, "work"); + assert.include( + input.args, + "http://forgejo.local:3000/api/v1/repos/maria/project/pulls/42", + ); + } + return Effect.succeed( + input.args[0] === "login" + ? processOutput( + encodeJson([ + { + name: "work", + url: "http://forgejo.local:3000", + ssh_host: "forgejo.local", + user: "maria", + default: "true", + }, + ]), + ) + : processOutput('{"message":"not found"}', { stderr: "HTTP/1.1 404 Not Found\n" }), + ); + }, + }, + }), + ), + ), +); + +it.effect("routes mounted Forgejo repositories without repeating the mount in API paths", () => + Effect.gen(function* () { + const cli = yield* ForgejoCli.make; + const viewer = yield* cli.api({ cwd: "/upstream-only", host: "code.test", path: "user" }); + assert.strictEqual(viewer.stdout, "[]"); + const mountedRepository = yield* cli.resolveRepository({ + cwd: "/upstream-only", + host: "code.test", + repository: "maria/project", + }); + assert.strictEqual(mountedRepository.baseUrl, "https://code.test/forgejo"); + assert.strictEqual(mountedRepository.repository, "maria/project"); + for (const path of [ + "repos/forgejo/maria/project/pulls?state=open", + "repos/forgejo/maria/project", + "repos/reviewer/project/contents/file.ts", + ]) { + const result = yield* cli.api({ + cwd: "/repo", + repository: "forgejo/maria/project", + context: { + provider: { kind: "forgejo", name: "Forgejo", baseUrl: "https://code.test/forgejo" }, + remoteName: "origin", + remoteUrl: "https://code.test/forgejo/maria/project.git", + }, + path, + }); + assert.strictEqual(result.stdout, "[]"); + } + const sameOwnerAsMount = yield* cli.resolveRepository({ + cwd: "/repo", + repository: "forgejo/project", + context: { + provider: { kind: "forgejo", name: "Forgejo", baseUrl: "https://code.test/forgejo" }, + remoteName: "origin", + remoteUrl: "ssh://git@code.test/forgejo/project.git", + }, + }); + assert.strictEqual(sameOwnerAsMount.command, "tea"); + assert.strictEqual(sameOwnerAsMount.repository, "forgejo/project"); + }).pipe( + Effect.provideService( + FileSystem.FileSystem, + FileSystem.makeNoop({ + exists: () => Effect.succeed(true), + readFileString: () => + Effect.succeed( + encodeJson({ + hosts: { "code.test/forgejo": { type: "Application", token: "test-token" } }, + }), + ), + }), + ), + Effect.provideService( + HttpClient.HttpClient, + HttpClient.make(() => { + throw new Error("tea must handle its own HTTP request"); + }), + ), + Effect.provide( + TestSourceControlHost.layer({ + process: { + run: (input) => { + if (input.command === "git") + return Effect.succeed( + processOutput("", { exitCode: ChildProcessSpawner.ExitCode(2) }), + ); + if (input.args[0] === "login") + return Effect.succeed( + processOutput( + encodeJson([ + { + name: "mounted", + url: "https://code.test/forgejo", + ssh_host: "code.test", + user: "maria", + default: "true", + }, + ]), + ), + ); + const supported = [ + "https://code.test/forgejo/api/v1/user", + "https://code.test/forgejo/api/v1/repos/maria/project/pulls?state=open", + "https://code.test/forgejo/api/v1/repos/maria/project", + "https://code.test/forgejo/api/v1/repos/reviewer/project/contents/file.ts", + ]; + if (input.args.at(-1)?.endsWith("/user")) assert.notInclude(input.args, "--repo"); + assert.strictEqual(input.command, "tea"); + return Effect.succeed( + supported.includes(input.args.at(-1) ?? "") + ? processOutput("[]", { stderr: "HTTP/1.1 200 OK\n" }) + : processOutput("{}", { stderr: "HTTP/1.1 404 Not Found\n" }), + ); + }, + }, + }), + ), + ), +); + +it.effect("prefers fj for HTTP and ported SSH aliases on root servers", () => { + const commands: string[] = []; + const requests: string[] = []; + return Effect.gen(function* () { + const cli = yield* ForgejoCli.make; + for (const remoteUrl of [ + "http://forgejo.local:3000/maria/project.git", + "ssh://git@ssh.forgejo.local:2222/maria/project.git", + "ssh://git@forgejo.local:2222/maria/project.git", + ]) { + const result = yield* cli.api({ + cwd: "/repo", + repository: "maria/project", + context: { + provider: { + kind: "forgejo", + name: "Forgejo", + baseUrl: "http://forgejo.local:3000", + }, + remoteName: "origin", + remoteUrl, + requestedHost: "forgejo.local:3000", + }, + path: "repos/maria/project/issues/42/comments", + method: "POST", + body: { body: "verified through fj" }, + }); + assert.strictEqual(result.stdout, '{"id":99}'); + } + assert.deepStrictEqual(commands, ["fj"]); + assert.deepStrictEqual(requests, [ + "http://forgejo.local:3000/api/v1/repos/maria/project/issues/42/comments", + "http://forgejo.local:3000/api/v1/repos/maria/project/issues/42/comments", + "http://forgejo.local:3000/api/v1/repos/maria/project/issues/42/comments", + ]); + const viewer = yield* cli.api({ + cwd: "/no-remotes", + host: "forgejo.local:3000", + path: "user", + }); + assert.strictEqual(viewer.stdout, '{"login":"maria"}'); + assert.strictEqual(requests.at(-1), "https://forgejo.local:3000/api/v1/user"); + const upstreamViewer = yield* cli.api({ + cwd: "/upstream-only", + host: "forgejo.local:3000", + path: "user", + }); + assert.strictEqual(upstreamViewer.stdout, '{"login":"maria"}'); + assert.strictEqual(requests.at(-1), "http://forgejo.local:3000/api/v1/user"); + const upstreamRepository = yield* cli.resolveRepository({ + cwd: "/upstream-only", + host: "forgejo.local:3000", + repository: "maria/project", + }); + assert.strictEqual(upstreamRepository.baseUrl, "http://forgejo.local:3000"); + assert.strictEqual(upstreamRepository.repository, "maria/project"); + const httpViewer = yield* cli.api({ cwd: "/repo", host: "forgejo.local:3000", path: "user" }); + assert.strictEqual(httpViewer.stdout, '{"login":"maria"}'); + assert.strictEqual(requests.at(-1), "http://forgejo.local:3000/api/v1/user"); + }).pipe( + Effect.provideService( + FileSystem.FileSystem, + FileSystem.makeNoop({ + exists: () => Effect.succeed(true), + readFileString: () => + Effect.succeed( + encodeJson({ + hosts: { + "forgejo.local:3000": { type: "Application", token: "test-token" }, + "forgejo.local:4000": { type: "Application", token: "other-token" }, + }, + aliases: { "ssh.forgejo.local:2222": "forgejo.local:3000" }, + }), + ), + }), + ), + Effect.provideService( + HttpClient.HttpClient, + HttpClient.make((request) => { + requests.push(request.url); + if (request.url.endsWith("/user")) { + assert.strictEqual(request.method, "GET"); + assert.strictEqual(request.headers.authorization, "token test-token"); + return Effect.succeed( + HttpClientResponse.fromWeb(request, new Response('{"login":"maria"}')), + ); + } + assert.strictEqual(request.method, "POST"); + assert.strictEqual(request.headers.authorization, "token test-token"); + assert.strictEqual(request.body._tag, "Uint8Array"); + if (request.body._tag === "Uint8Array") + assert.deepStrictEqual(JSON.parse(new TextDecoder().decode(request.body.body)), { + body: "verified through fj", + }); + return Effect.succeed( + HttpClientResponse.fromWeb(request, new Response('{"id":99}', { status: 201 })), + ); + }), + ), + Effect.provide( + TestSourceControlHost.layer({ + process: { + run: (input) => { + commands.push(input.command); + if (input.command === "git") + return Effect.succeed( + input.cwd === "/no-remotes" + ? processOutput("", { exitCode: ChildProcessSpawner.ExitCode(2) }) + : processOutput( + `${input.cwd === "/upstream-only" ? "upstream" : "origin"}\thttp://forgejo.local:3000/maria/project.git (fetch)\nother\thttp://forgejo.local:3000/maria/other.git (fetch)\nunrelated\thttp://other.local:3000/maria/project.git (fetch)`, + ), + ); + assert.strictEqual(input.command, "fj"); + assert.deepStrictEqual(input.args, [ + "--host", + input.cwd === "/no-remotes" + ? "https://forgejo.local:3000" + : "http://forgejo.local:3000", + "whoami", + ]); + return Effect.succeed(processOutput("")); + }, + }, + }), + ), + ); +}); + +it.effect("loads later fj review pages when the server caps pages below the requested size", () => { + const pages: number[] = []; + let issueCommentRequests = 0; + return Effect.gen(function* () { + const cli = yield* ForgejoCli.make; + const provider = yield* ForgejoPullRequestProvider.make.pipe( + Effect.provideService(ForgejoCli.ForgejoCli, cli), + ); + const activity = yield* provider.getChangeRequestActivity({ + cwd: "/repo", + repository: "maria/project", + host: "forgejo.test", + number: 42, + }); + assert.strictEqual(activity.commentCount, 42); + assert.strictEqual(activity.comments.at(-1)?.id, "review:41"); + assert.strictEqual(activity.commentsTruncated, false); + assert.deepStrictEqual(pages, [1, 2, 3]); + assert.strictEqual(issueCommentRequests, 1); + }).pipe( + Effect.provideService( + FileSystem.FileSystem, + FileSystem.makeNoop({ + exists: () => Effect.succeed(true), + readFileString: () => + Effect.succeed( + encodeJson({ hosts: { "forgejo.test": { type: "Application", token: "test-token" } } }), + ), + }), + ), + Effect.provideService( + HttpClient.HttpClient, + HttpClient.make((request) => { + const url = new URL(request.url); + if (url.pathname === "/api/v1/repos/maria/project/issues/42/comments") { + issueCommentRequests++; + return Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response( + encodeJson([ + { + id: 100, + body: "Unpaginated issue comment", + user: { login: "maria" }, + created_at: "2026-09-13T00:00:00Z", + }, + ]), + ), + ), + ); + } + if (url.pathname.endsWith("/reviews")) { + const page = Number(url.searchParams.get("page")); + pages.push(page); + assert.ok(page >= 1 && page <= 3); + const reviews = Array.from({ length: page < 3 ? 20 : 1 }, (_, index) => ({ + id: (page - 1) * 20 + index + 1, + body: "Review from a capped page", + user: { login: "maria" }, + state: "COMMENT", + submitted_at: "2026-09-13T00:00:00Z", + comments_count: 0, + })); + return Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response(encodeJson(reviews), { + headers: + page === 2 + ? {} + : { + Link: + page === 1 + ? `<${url.origin}${url.pathname}?limit=50&page=2>; rel="next"` + : `<${url.origin}${url.pathname}?limit=50&page=1>; rel="prev"`, + }, + }), + ), + ); + } + return Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response(encodeJson(url.pathname === "/api/v1/user" ? { login: "maria" } : [])), + ), + ); + }), + ), + Effect.provide( + TestSourceControlHost.layer({ + process: { + run: (input) => { + if (input.command === "git") { + assert.deepStrictEqual(input.args, ["remote", "-v"]); + return Effect.succeed( + processOutput("origin\thttps://forgejo.test/maria/project.git (fetch)"), + ); + } + assert.strictEqual(input.command, "fj"); + assert.deepStrictEqual(input.args, ["--host", "https://forgejo.test", "whoami"]); + return Effect.succeed(processOutput("")); + }, + }, + }), + ), + ); +}); + +it.effect("falls back to tea when fj is missing or has no account for this server", () => + Effect.gen(function* () { + for (const scenario of ["missing-cli", "missing-account", "stale-invalid-storage"] as const) { + const commands: string[] = []; + yield* Effect.gen(function* () { + const cli = yield* ForgejoCli.make; + const result = yield* cli.api({ + cwd: "/repo", + repository: "https://forgejo.local:3000/maria/project", + path: "repos/maria/project/pulls", + }); + assert.strictEqual(result.stdout, "[]"); + assert.deepStrictEqual( + commands, + scenario === "missing-account" ? ["tea", "tea"] : ["fj", "tea", "tea"], + ); + const viewer = yield* cli.api({ + cwd: "/upstream-only", + host: "forgejo.local:3000", + path: "user", + }); + assert.strictEqual(viewer.stdout, "[]"); + }).pipe( + Effect.provideService( + FileSystem.FileSystem, + FileSystem.makeNoop({ + exists: () => Effect.succeed(true), + readFileString: () => + Effect.succeed( + scenario === "stale-invalid-storage" + ? "invalid json" + : encodeJson({ + hosts: { + [scenario === "missing-cli" ? "forgejo.local:3000" : "other.local"]: { + type: "Application", + token: "test-token", + }, + }, + }), + ), + }), + ), + Effect.provideService( + HttpClient.HttpClient, + HttpClient.make(() => { + throw new Error("tea must handle its own HTTP request"); + }), + ), + Effect.provide( + TestSourceControlHost.layer({ + process: { + run: (input) => { + commands.push(input.command); + if (input.command === "git") + return Effect.succeed( + processOutput("", { exitCode: ChildProcessSpawner.ExitCode(2) }), + ); + if (input.command === "fj") + return Effect.fail( + new VcsProcessSpawnError({ + operation: input.operation, + command: input.command, + cwd: input.cwd, + cause: new Error("fj not found"), + }), + ); + assert.strictEqual(input.command, "tea"); + if (input.args.at(-1)?.endsWith("/user")) assert.notInclude(input.args, "--repo"); + return Effect.succeed( + input.args[0] === "login" + ? processOutput( + encodeJson([ + { + name: "work", + url: "https://forgejo.local:3000", + user: "maria", + default: "true", + valid: "true", + }, + ]), + ) + : processOutput("[]", { stderr: "HTTP/1.1 200 OK\n" }), + ); + }, + }, + }), + ), + ); + } + }), +); + +it.effect("handles fj mutation statuses without retrying failures or reading absent bodies", () => + Effect.gen(function* () { + for (const status of [204, 205, 302, 401, 403, 404, 429, 500]) { + let writes = 0; + yield* Effect.gen(function* () { + const cli = yield* ForgejoCli.make; + const result = yield* cli + .api({ + cwd: "/repo", + repository: "https://forgejo.local/maria/project", + path: "repos/maria/project/issues/42/comments", + method: "POST", + body: { body: "only once" }, + }) + .pipe(Effect.result); + assert.strictEqual(result._tag, status < 300 ? "Success" : "Failure"); + if (result._tag === "Success") assert.strictEqual(result.success.stdout, ""); + if (result._tag === "Failure") { + assert.strictEqual(result.failure.command, "fj"); + assert.strictEqual(result.failure.httpStatus, status); + } + assert.strictEqual(writes, 1); + }).pipe( + Effect.provideService( + FileSystem.FileSystem, + FileSystem.makeNoop({ + exists: () => Effect.succeed(true), + readFileString: () => + Effect.succeed( + encodeJson({ + hosts: { + "forgejo.local": { type: "Application", token: "test-token" }, + }, + }), + ), + }), + ), + Effect.provideService( + HttpClient.HttpClient, + HttpClient.make((request) => { + writes++; + assert.strictEqual( + request.url, + "https://forgejo.local/api/v1/repos/maria/project/issues/42/comments", + ); + return Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response(status < 300 ? null : "", { + status, + headers: { location: "https://other.local/" }, + }), + ), + ); + }), + ), + Effect.provide( + TestSourceControlHost.layer({ + process: { + run: (input) => { + assert.strictEqual( + input.command, + "fj", + "a failed mutation must never switch accounts or CLI", + ); + return Effect.succeed(processOutput("")); + }, + }, + }), + ), + ); + } + }), +); + +it.effect( + "discovers fj first and retains configured authentication failures instead of switching accounts", + () => + Effect.gen(function* () { + for (const scenario of ["authenticated", "revoked", "missing", "invalid-storage"] as const) { + const commands: string[] = []; + yield* Effect.gen(function* () { + const spec = yield* ForgejoSourceControlProvider.makeDiscovery; + assert.strictEqual(spec.type, "managed-cli"); + if (spec.type !== "managed-cli") return; + const result = yield* spec.probe("/repo"); + assert.strictEqual(result.executable, scenario === "missing" ? "tea" : "fj"); + assert.strictEqual( + result.auth.status, + scenario === "revoked" + ? "unauthenticated" + : scenario === "invalid-storage" + ? "unknown" + : "authenticated", + ); + if (scenario !== "revoked" && scenario !== "invalid-storage") + assert.deepStrictEqual(result.auth.host, Option.some("forgejo.local:3000")); + assert.deepStrictEqual( + result.auth.account, + scenario === "authenticated" || scenario === "missing" + ? Option.some("maria") + : Option.none(), + ); + assert.strictEqual( + commands.some((command) => command.startsWith("tea ")), + scenario === "missing", + ); + assert.include(commands, "fj version"); + }).pipe( + Effect.provide( + Layer.mergeAll( + Layer.mock(ForgejoCli.ForgejoCli)({ + getAccount: (input) => { + assert.strictEqual(scenario, "authenticated"); + assert.deepStrictEqual(input, { + cwd: "/repo", + baseUrl: "http://forgejo.local:3000", + }); + return Effect.succeed("maria"); + }, + listLogins: (input) => { + assert.strictEqual( + input.remoteUrl, + "http://forgejo.local:3000/maria/project.git", + ); + if (scenario === "invalid-storage") + return Effect.fail( + new ForgejoCli.ForgejoCliError({ + command: "fj", + cwd: input.cwd, + reason: "authentication", + detail: "fj authentication storage is invalid.", + }), + ); + return Effect.succeed([ + { + name: "forgejo.local:3000", + url: "http://forgejo.local:3000", + user: "", + default: "false", + }, + ]); + }, + }), + TestSourceControlHost.layer({ + process: { + run: (input) => { + commands.push(`${input.command} ${input.args.join(" ")}`); + if (input.command === "git") + return Effect.succeed( + processOutput("http://forgejo.local:3000/maria/project.git\n"), + ); + if (input.command === "fj") { + if (scenario === "missing") + return Effect.fail( + new VcsProcessSpawnError({ + operation: input.operation, + command: input.command, + cwd: input.cwd, + cause: new Error("fj not found"), + }), + ); + if (input.args[0] === "version") + return Effect.succeed(processOutput("fj 0.10.0")); + if (scenario === "invalid-storage") { + assert.deepStrictEqual(input.args, ["auth", "list"]); + return Effect.succeed(processOutput("")); + } + assert.deepStrictEqual(input.args, [ + "--host", + "http://forgejo.local:3000", + "whoami", + ]); + return Effect.succeed( + processOutput("", { + exitCode: ChildProcessSpawner.ExitCode(scenario === "revoked" ? 1 : 0), + }), + ); + } + assert.strictEqual(input.command, "tea"); + return Effect.succeed( + input.args[0] === "--version" + ? processOutput("tea version 0.16.0") + : processOutput( + encodeJson([ + { + name: "work", + url: "http://forgejo.local:3000", + user: "maria", + default: "true", + valid: "true", + }, + ]), + ), + ); + }, + }, + }), + ), + ), + ); + } + }), +); diff --git a/apps/server/src/sourceControl/ForgejoSourceControlProvider.ts b/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.ts similarity index 98% rename from apps/server/src/sourceControl/ForgejoSourceControlProvider.ts rename to packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.ts index 79a1ca6f2aa0..cbcb3405f4ec 100644 --- a/apps/server/src/sourceControl/ForgejoSourceControlProvider.ts +++ b/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.ts @@ -3,7 +3,7 @@ import * as FileSystem from "effect/FileSystem"; import * as Schema from "effect/Schema"; import * as Result from "effect/Result"; import { SourceControlProviderError } from "@t3tools/contracts"; -import * as VcsProcess from "../vcs/VcsProcess.ts"; +import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; import * as ForgejoCli from "./ForgejoCli.ts"; import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; import { @@ -55,7 +55,7 @@ export const discovery = { export const makeDiscovery = Effect.gen(function* () { const cli = yield* ForgejoCli.ForgejoCli; - const process = yield* VcsProcess.VcsProcess; + const { process } = yield* SourceControlHost.SourceControlHost; const listLogins = cli.listLogins; if (!listLogins) return discovery; return { @@ -175,7 +175,7 @@ const repositoryPath = (repository: string) => export const make = Effect.gen(function* () { const cli = yield* ForgejoCli.ForgejoCli; const fs = yield* FileSystem.FileSystem; - const process = yield* VcsProcess.VcsProcess; + const { process } = yield* SourceControlHost.SourceControlHost; const request = >( input: ForgejoCli.ForgejoApiInput, schema: S, diff --git a/packages/source-control-forgejo/src/server/driver.ts b/packages/source-control-forgejo/src/server/driver.ts new file mode 100644 index 000000000000..ae76d7c6409a --- /dev/null +++ b/packages/source-control-forgejo/src/server/driver.ts @@ -0,0 +1,20 @@ +/** + * The Forgejo / Gitea driver: repository operations, discovery, and pull requests through + * `fj` or `tea`. + * + * @module source-control-forgejo/server/driver + */ +import { defineSourceControlDriver } from "@t3tools/source-control-core/server/driver"; +import * as Effect from "effect/Effect"; + +import * as ForgejoPullRequestProvider from "./ForgejoPullRequestProvider.ts"; +import * as ForgejoSourceControlProvider from "./ForgejoSourceControlProvider.ts"; + +export const driver = defineSourceControlDriver({ + kind: "forgejo", + make: Effect.all({ + sourceControl: ForgejoSourceControlProvider.make, + discovery: ForgejoSourceControlProvider.makeDiscovery, + pullRequests: ForgejoPullRequestProvider.make, + }), +}); diff --git a/apps/server/src/pullRequest/forgejoPullRequestJson.ts b/packages/source-control-forgejo/src/server/forgejoPullRequestJson.ts similarity index 100% rename from apps/server/src/pullRequest/forgejoPullRequestJson.ts rename to packages/source-control-forgejo/src/server/forgejoPullRequestJson.ts diff --git a/apps/server/src/sourceControl/forgejoPullRequests.ts b/packages/source-control-forgejo/src/server/forgejoPullRequests.ts similarity index 100% rename from apps/server/src/sourceControl/forgejoPullRequests.ts rename to packages/source-control-forgejo/src/server/forgejoPullRequests.ts diff --git a/packages/source-control-forgejo/tsconfig.json b/packages/source-control-forgejo/tsconfig.json new file mode 100644 index 000000000000..374bac55202d --- /dev/null +++ b/packages/source-control-forgejo/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["src"] +} diff --git a/packages/source-control-testing/package.json b/packages/source-control-testing/package.json new file mode 100644 index 000000000000..66426fe03377 --- /dev/null +++ b/packages/source-control-testing/package.json @@ -0,0 +1,22 @@ +{ + "name": "@t3tools/source-control-testing", + "private": true, + "type": "module", + "exports": { + "./TestSourceControlHost": { + "types": "./src/TestSourceControlHost.ts", + "import": "./src/TestSourceControlHost.ts" + } + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@t3tools/contracts": "workspace:*", + "@t3tools/source-control-core": "workspace:*", + "effect": "catalog:" + }, + "devDependencies": { + "@types/node": "catalog:" + } +} diff --git a/packages/source-control-testing/src/TestSourceControlHost.ts b/packages/source-control-testing/src/TestSourceControlHost.ts new file mode 100644 index 000000000000..293339d0867e --- /dev/null +++ b/packages/source-control-testing/src/TestSourceControlHost.ts @@ -0,0 +1,47 @@ +/** + * A `SourceControlHost.SourceControlHost` for provider tests. Settings are fixed unless the test + * supplies its own, and every process run goes to the test's `run`, which fails by default so an + * unexpected CLI call is visible. + * + * @module source-control-testing/TestSourceControlHost + */ +import { DEFAULT_SERVER_SETTINGS, type ServerSettings } from "@t3tools/contracts"; +import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; + +export interface TestSourceControlHostOptions { + readonly settings?: ServerSettings; + readonly process?: Partial; +} + +export const layer = ( + options: TestSourceControlHostOptions = {}, +): Layer.Layer => + Layer.succeed( + SourceControlHost.SourceControlHost, + SourceControlHost.SourceControlHost.of({ + settings: { get: Effect.succeed(options.settings ?? DEFAULT_SERVER_SETTINGS) }, + process: { + run: + options.process?.run ?? + ((input) => Effect.die(`Unexpected ${input.command} run in ${input.operation}.`)), + }, + }), + ); + +/** A successful run's output, for tests that script CLI responses. */ +export const processOutput = ( + stdout: string, + options?: { + readonly stderr?: string; + readonly exitCode?: ChildProcessSpawner.ExitCode; + }, +): SourceControlHost.SourceControlProcessOutput => ({ + exitCode: options?.exitCode ?? ChildProcessSpawner.ExitCode(0), + stdout, + stderr: options?.stderr ?? "", + stdoutTruncated: false, + stderrTruncated: false, +}); diff --git a/packages/source-control-testing/tsconfig.json b/packages/source-control-testing/tsconfig.json new file mode 100644 index 000000000000..374bac55202d --- /dev/null +++ b/packages/source-control-testing/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 387677cd3d52..e91a9bb0555f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -651,6 +651,9 @@ importers: '@t3tools/source-control-core': specifier: workspace:* version: link:../../packages/source-control-core + '@t3tools/source-control-forgejo': + specifier: workspace:* + version: link:../../packages/source-control-forgejo diff: specifier: 8.0.3 version: 8.0.3 @@ -1518,6 +1521,9 @@ importers: '@t3tools/contracts': specifier: workspace:* version: link:../contracts + '@t3tools/shared': + specifier: workspace:* + version: link:../shared effect: specifier: 4.0.2 version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) @@ -1532,6 +1538,53 @@ importers: specifier: 1.0.0 version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + packages/source-control-forgejo: + dependencies: + '@t3tools/contracts': + specifier: workspace:* + version: link:../contracts + '@t3tools/provider-core': + specifier: workspace:* + version: link:../provider-core + '@t3tools/shared': + specifier: workspace:* + version: link:../shared + '@t3tools/source-control-core': + specifier: workspace:* + version: link:../source-control-core + effect: + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) + devDependencies: + '@effect/vitest': + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + '@t3tools/source-control-testing': + specifier: workspace:* + version: link:../source-control-testing + '@types/node': + specifier: 24.12.4 + version: 24.12.4 + vite-plus: + specifier: 1.0.0 + version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + + packages/source-control-testing: + dependencies: + '@t3tools/contracts': + specifier: workspace:* + version: link:../contracts + '@t3tools/source-control-core': + specifier: workspace:* + version: link:../source-control-core + effect: + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) + devDependencies: + '@types/node': + specifier: 24.12.4 + version: 24.12.4 + packages/ssh: dependencies: '@t3tools/contracts': diff --git a/scripts/release-smoke.ts b/scripts/release-smoke.ts index f1c903bc8fe1..6482dd28f295 100644 --- a/scripts/release-smoke.ts +++ b/scripts/release-smoke.ts @@ -38,6 +38,8 @@ const workspaceFiles = [ "packages/provider-muse/package.json", "packages/provider-opencode/package.json", "packages/source-control-core/package.json", + "packages/source-control-forgejo/package.json", + "packages/source-control-testing/package.json", "packages/effect-acp/package.json", "packages/effect-codex-app-server/package.json", "scripts/package.json", From d01febb509a376d93af2d298a7787449792b5fcb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 14:50:00 -0700 Subject: [PATCH 12/39] refactor(source-control): Azure DevOps lives in @t3tools/source-control-azure-devops (#17592) Co-authored-by: Claude Opus 5.5 --- apps/server/package.json | 2 +- apps/server/scripts/evaluate-thread-titles.ts | 2 - .../PullRequestProviderRateLimit.test.ts | 4 +- .../PullRequestProviderRegistry.ts | 5 -- apps/server/src/server.ts | 2 - .../SourceControlDiscovery.test.ts | 4 +- .../SourceControlProviderRegistry.test.ts | 4 +- .../SourceControlProviderRegistry.ts | 16 +++--- .../src/sourceControl/builtInDrivers.ts | 20 ++++++- apps/server/src/ws.ts | 2 - docs/internals/pull-request-file-revisions.md | 2 +- package.json | 2 +- .../source-control-azure-devops/package.json | 57 +++++++++++++++++++ .../src/server}/AzureDevOpsCli.test.ts | 11 ++-- .../src/server}/AzureDevOpsCli.ts | 8 +-- .../server}/AzureDevOpsPullRequestCli.test.ts | 2 +- .../src/server}/AzureDevOpsPullRequestCli.ts | 4 +- .../AzureDevOpsPullRequestProvider.test.ts | 0 .../server}/AzureDevOpsPullRequestProvider.ts | 0 .../AzureDevOpsSourceControlProvider.test.ts | 0 .../AzureDevOpsSourceControlProvider.ts | 0 .../src/server}/azureDevOpsDiff.test.ts | 0 .../src/server}/azureDevOpsDiff.ts | 0 .../azureDevOpsPullRequestJson.test.ts | 0 .../src/server}/azureDevOpsPullRequestJson.ts | 2 +- .../src/server}/azureDevOpsPullRequests.ts | 0 .../src/server/driver.ts | 19 +++++++ .../source-control-azure-devops/tsconfig.json | 7 +++ pnpm-lock.yaml | 40 ++++++++++++- scripts/release-smoke.ts | 1 + 30 files changed, 169 insertions(+), 47 deletions(-) create mode 100644 packages/source-control-azure-devops/package.json rename {apps/server/src/sourceControl => packages/source-control-azure-devops/src/server}/AzureDevOpsCli.test.ts (97%) rename {apps/server/src/sourceControl => packages/source-control-azure-devops/src/server}/AzureDevOpsCli.ts (98%) rename {apps/server/src/pullRequest => packages/source-control-azure-devops/src/server}/AzureDevOpsPullRequestCli.test.ts (99%) rename {apps/server/src/pullRequest => packages/source-control-azure-devops/src/server}/AzureDevOpsPullRequestCli.ts (99%) rename {apps/server/src/pullRequest => packages/source-control-azure-devops/src/server}/AzureDevOpsPullRequestProvider.test.ts (100%) rename {apps/server/src/pullRequest => packages/source-control-azure-devops/src/server}/AzureDevOpsPullRequestProvider.ts (100%) rename {apps/server/src/sourceControl => packages/source-control-azure-devops/src/server}/AzureDevOpsSourceControlProvider.test.ts (100%) rename {apps/server/src/sourceControl => packages/source-control-azure-devops/src/server}/AzureDevOpsSourceControlProvider.ts (100%) rename {apps/server/src/pullRequest => packages/source-control-azure-devops/src/server}/azureDevOpsDiff.test.ts (100%) rename {apps/server/src/pullRequest => packages/source-control-azure-devops/src/server}/azureDevOpsDiff.ts (100%) rename {apps/server/src/pullRequest => packages/source-control-azure-devops/src/server}/azureDevOpsPullRequestJson.test.ts (100%) rename {apps/server/src/pullRequest => packages/source-control-azure-devops/src/server}/azureDevOpsPullRequestJson.ts (99%) rename {apps/server/src/sourceControl => packages/source-control-azure-devops/src/server}/azureDevOpsPullRequests.ts (100%) create mode 100644 packages/source-control-azure-devops/src/server/driver.ts create mode 100644 packages/source-control-azure-devops/tsconfig.json diff --git a/apps/server/package.json b/apps/server/package.json index 17b77a22244a..232a99960f96 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -45,9 +45,9 @@ "@t3tools/provider-muse": "workspace:*", "@t3tools/provider-opencode": "workspace:*", "@t3tools/provider-pi": "workspace:*", + "@t3tools/source-control-azure-devops": "workspace:*", "@t3tools/source-control-core": "workspace:*", "@t3tools/source-control-forgejo": "workspace:*", - "diff": "8.0.3", "effect": "catalog:", "jose": "catalog:", "node-pty": "^1.2.0-beta.15", diff --git a/apps/server/scripts/evaluate-thread-titles.ts b/apps/server/scripts/evaluate-thread-titles.ts index 3ebd7cbaf8d5..92313a0869c7 100644 --- a/apps/server/scripts/evaluate-thread-titles.ts +++ b/apps/server/scripts/evaluate-thread-titles.ts @@ -26,7 +26,6 @@ import * as SourceControlProviderRegistry from "../src/sourceControl/SourceContr import * as GitHubApi from "../src/sourceControl/GitHubApi.ts"; import * as GitLabCli from "../src/sourceControl/GitLabCli.ts"; import * as SourceControlBuiltInDrivers from "../src/sourceControl/builtInDrivers.ts"; -import * as AzureDevOpsCli from "../src/sourceControl/AzureDevOpsCli.ts"; import * as BitbucketApi from "../src/sourceControl/BitbucketApi.ts"; import * as ServerSettings from "../src/serverSettings.ts"; import * as VcsProcess from "../src/vcs/VcsProcess.ts"; @@ -155,7 +154,6 @@ await Effect.runPromise( GitHubApi.layerWithDependencies, GitLabCli.layer, SourceControlBuiltInDrivers.layer, - AzureDevOpsCli.layer, // No saved credentials here; Bitbucket falls back to T3CODE_BITBUCKET_* variables. BitbucketApi.layer, ), diff --git a/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts b/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts index ca12da2b8979..38f587353980 100644 --- a/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts +++ b/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts @@ -1,11 +1,11 @@ import { assert, it } from "@effect/vitest"; -import * as AzureDevOpsCli from "../sourceControl/AzureDevOpsCli.ts"; +import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsCli"; import * as BitbucketApi from "../sourceControl/BitbucketApi.ts"; import * as GitHubApi from "../sourceControl/GitHubApi.ts"; import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; import * as GitLabCli from "../sourceControl/GitLabCli.ts"; -import { azureDevOpsProviderFailure } from "./AzureDevOpsPullRequestProvider.ts"; +import { azureDevOpsProviderFailure } from "@t3tools/source-control-azure-devops/server/AzureDevOpsPullRequestProvider"; import { bitbucketProviderFailure } from "./BitbucketPullRequestProvider.ts"; import { gitHubProviderFailure } from "./GitHubPullRequestProvider.ts"; import { gitLabProviderFailure } from "./GitLabPullRequestProvider.ts"; diff --git a/apps/server/src/pullRequest/PullRequestProviderRegistry.ts b/apps/server/src/pullRequest/PullRequestProviderRegistry.ts index 3c2ad367d446..009d3d3f8b4f 100644 --- a/apps/server/src/pullRequest/PullRequestProviderRegistry.ts +++ b/apps/server/src/pullRequest/PullRequestProviderRegistry.ts @@ -3,12 +3,9 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import type { SourceControlProviderKind } from "@t3tools/contracts"; -import * as AzureDevOpsCli from "../sourceControl/AzureDevOpsCli.ts"; import * as BitbucketApi from "../sourceControl/BitbucketApi.ts"; import * as GitHubApi from "../sourceControl/GitHubApi.ts"; import * as GitLabCli from "../sourceControl/GitLabCli.ts"; -import * as AzureDevOpsPullRequestCli from "./AzureDevOpsPullRequestCli.ts"; -import * as AzureDevOpsPullRequestProvider from "./AzureDevOpsPullRequestProvider.ts"; import * as BitbucketPullRequestApi from "./BitbucketPullRequestApi.ts"; import * as BitbucketPullRequestProvider from "./BitbucketPullRequestProvider.ts"; import * as GitHubPullRequestApi from "./GitHubPullRequestApi.ts"; @@ -49,7 +46,6 @@ export const make = Effect.gen(function* () { GitHubPullRequestProvider.make, GitLabPullRequestProvider.make, BitbucketPullRequestProvider.make, - AzureDevOpsPullRequestProvider.make, ]); const drivers = yield* Effect.forEach(BuiltInDrivers.BUILT_IN_SOURCE_CONTROL_DRIVERS, (driver) => driver.make.pipe(Effect.map((instance) => instance.pullRequests)), @@ -72,5 +68,4 @@ export const layer = Layer.effect(PullRequestProviderRegistry, make).pipe( Layer.provide(GitLabPullRequestCli.layer.pipe(Layer.provide(GitLabCli.layer))), Layer.provide(BuiltInDrivers.layer), Layer.provide(BitbucketPullRequestApi.layer.pipe(Layer.provide(BitbucketApi.layer))), - Layer.provide(AzureDevOpsPullRequestCli.layer.pipe(Layer.provide(AzureDevOpsCli.layer))), ); diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index b1ac0b12dbd3..82c5de077ef7 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -52,7 +52,6 @@ import * as ProviderHostLive from "./provider/ProviderHostLive.ts"; import * as AcpRegistrySupport from "@t3tools/provider-acp-registry/server/AcpRegistrySupport"; import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts"; import * as CheckpointStore from "./checkpointing/CheckpointStore.ts"; -import * as AzureDevOpsCli from "./sourceControl/AzureDevOpsCli.ts"; import * as BitbucketApi from "./sourceControl/BitbucketApi.ts"; import * as GitHubApi from "./sourceControl/GitHubApi.ts"; import * as GitLabCli from "./sourceControl/GitLabCli.ts"; @@ -301,7 +300,6 @@ const layerVcsDriverRegistry = VcsDriverRegistry.layer.pipe(Layer.provide(VcsPro const layerSourceControlProviderRegistry = SourceControlProviderRegistry.layer.pipe( Layer.provide( Layer.mergeAll( - AzureDevOpsCli.layer, BitbucketApi.layer, GitHubApi.layerWithDependencies, GitLabCli.layer, diff --git a/apps/server/src/sourceControl/SourceControlDiscovery.test.ts b/apps/server/src/sourceControl/SourceControlDiscovery.test.ts index fe7694cef545..e818a9274635 100644 --- a/apps/server/src/sourceControl/SourceControlDiscovery.test.ts +++ b/apps/server/src/sourceControl/SourceControlDiscovery.test.ts @@ -16,7 +16,8 @@ import * as ServerConfig from "../config.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; -import * as AzureDevOpsCli from "./AzureDevOpsCli.ts"; +import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsCli"; +import * as AzureDevOpsPullRequestCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsPullRequestCli"; import * as BitbucketApi from "./BitbucketApi.ts"; import * as GitHubApi from "./GitHubApi.ts"; import * as GitLabCli from "./GitLabCli.ts"; @@ -39,6 +40,7 @@ const layerSourceControlProviderRegistryTest = (input: { prefix: "t3-source-control-registry-test-", }).pipe(Layer.provide(NodeServices.layer)), Layer.mock(AzureDevOpsCli.AzureDevOpsCli)({}), + Layer.mock(AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli)({}), Layer.mock(BitbucketApi.BitbucketApi)(input.bitbucket), ServerSettings.ServerSettingsService.layerTest(), Layer.mock(GitHubApi.GitHubApi)({}), diff --git a/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts b/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts index 2e2969488d3d..5f48bae18588 100644 --- a/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts +++ b/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts @@ -13,7 +13,8 @@ import * as ServerConfig from "../config.ts"; import type * as VcsDriver from "../vcs/VcsDriver.ts"; import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; -import * as AzureDevOpsCli from "./AzureDevOpsCli.ts"; +import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsCli"; +import * as AzureDevOpsPullRequestCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsPullRequestCli"; import * as BitbucketApi from "./BitbucketApi.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as GitHubApi from "./GitHubApi.ts"; @@ -103,6 +104,7 @@ function makeRegistry(input: { ), ), Layer.mock(AzureDevOpsCli.AzureDevOpsCli)({}), + Layer.mock(AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli)({}), Layer.mock(BitbucketApi.BitbucketApi)({}), ServerSettings.ServerSettingsService.layerTest(), Layer.mock(GitHubApi.GitHubApi)({ diff --git a/apps/server/src/sourceControl/SourceControlProviderRegistry.ts b/apps/server/src/sourceControl/SourceControlProviderRegistry.ts index 22516853b836..948d583296fa 100644 --- a/apps/server/src/sourceControl/SourceControlProviderRegistry.ts +++ b/apps/server/src/sourceControl/SourceControlProviderRegistry.ts @@ -11,7 +11,6 @@ import { import type { SourceControlProviderKind } from "@t3tools/contracts"; import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; -import * as AzureDevOpsSourceControlProvider from "./AzureDevOpsSourceControlProvider.ts"; import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts"; @@ -310,7 +309,6 @@ export const make = Effect.gen(function* () { const gitlab = yield* GitLabSourceControlProvider.make; const bitbucket = yield* BitbucketSourceControlProvider.make; const bitbucketDiscovery = yield* BitbucketSourceControlProvider.makeDiscovery; - const azureDevOps = yield* AzureDevOpsSourceControlProvider.make; const drivers = yield* Effect.forEach(BuiltInDrivers.BUILT_IN_SOURCE_CONTROL_DRIVERS, (driver) => driver.make.pipe( Effect.map((instance): SourceControlProviderRegistration => ({ @@ -320,7 +318,9 @@ export const make = Effect.gen(function* () { })), ), ); - return yield* makeWithProviders([ + // Discovery lists hosts in this order, so it is kept while hosts move into packages. + const order = ["github", "gitlab", "azure-devops", "bitbucket", "forgejo"]; + const registrations: ReadonlyArray = [ { kind: "github", provider: github, @@ -331,18 +331,16 @@ export const make = Effect.gen(function* () { provider: gitlab, discovery: GitLabSourceControlProvider.discovery, }, - { - kind: "azure-devops", - provider: azureDevOps, - discovery: AzureDevOpsSourceControlProvider.discovery, - }, { kind: "bitbucket", provider: bitbucket, discovery: bitbucketDiscovery, }, ...drivers, - ]); + ]; + return yield* makeWithProviders( + registrations.toSorted((left, right) => order.indexOf(left.kind) - order.indexOf(right.kind)), + ); }); export const layer = Layer.effect(SourceControlProviderRegistry, make); diff --git a/apps/server/src/sourceControl/builtInDrivers.ts b/apps/server/src/sourceControl/builtInDrivers.ts index c49319ba0a1f..584bf139c0fb 100644 --- a/apps/server/src/sourceControl/builtInDrivers.ts +++ b/apps/server/src/sourceControl/builtInDrivers.ts @@ -7,14 +7,28 @@ * * @module sourceControl/builtInDrivers */ +import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsCli"; +import * as AzureDevOpsPullRequestCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsPullRequestCli"; +import * as AzureDevOpsDriver from "@t3tools/source-control-azure-devops/server/driver"; import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; import * as ForgejoDriver from "@t3tools/source-control-forgejo/server/driver"; +import type { SourceControlDriver } from "@t3tools/source-control-core/server/driver"; import * as Layer from "effect/Layer"; import * as ServerSourceControlHost from "./ServerSourceControlHost.ts"; -/** Ordered as the hosts appear in discovery, after the ones still built in the server. */ -export const BUILT_IN_SOURCE_CONTROL_DRIVERS = [ForgejoDriver.driver] as const; +const drivers = [AzureDevOpsDriver.driver, ForgejoDriver.driver]; + +/** Every service a built-in driver's `make` needs; the server's layers must provide them all. */ +export type BuiltInSourceControlDriversEnv = + (typeof drivers)[number] extends SourceControlDriver ? R : never; + +export const BUILT_IN_SOURCE_CONTROL_DRIVERS: ReadonlyArray< + SourceControlDriver +> = drivers; /** The services the built-in drivers' packages own, plus the host port they all run against. */ -export const layer = ForgejoCli.layer.pipe(Layer.provideMerge(ServerSourceControlHost.layer)); +export const layer = Layer.mergeAll( + AzureDevOpsPullRequestCli.layer.pipe(Layer.provideMerge(AzureDevOpsCli.layer)), + ForgejoCli.layer, +).pipe(Layer.provideMerge(ServerSourceControlHost.layer)); diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index d3e27ea0bc81..899f021eb937 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -229,7 +229,6 @@ import * as SqlClient from "effect/sql/SqlClient"; import * as PullRequestSyncReactor from "./orchestration-v2/PullRequestSyncReactor.ts"; import * as SourceControlDiscovery from "./sourceControl/SourceControlDiscovery.ts"; import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts"; -import * as AzureDevOpsCli from "./sourceControl/AzureDevOpsCli.ts"; import * as BitbucketApi from "./sourceControl/BitbucketApi.ts"; import * as GitHubApi from "./sourceControl/GitHubApi.ts"; import * as GitLabCli from "./sourceControl/GitLabCli.ts"; @@ -3204,7 +3203,6 @@ export const layer = Layer.unwrap( SourceControlProviderRegistry.layer.pipe( Layer.provide( Layer.mergeAll( - AzureDevOpsCli.layer, BitbucketApi.layer, GitHubApi.layerWithDependencies, GitLabCli.layer, diff --git a/docs/internals/pull-request-file-revisions.md b/docs/internals/pull-request-file-revisions.md index 22eeb63848ea..ceb80de8efe0 100644 --- a/docs/internals/pull-request-file-revisions.md +++ b/docs/internals/pull-request-file-revisions.md @@ -24,7 +24,7 @@ the provider never got to look at. Absence below the boundary does not survive i What "never got to look at" is belongs to the host, and the contract cannot know any of them: -- [Azure DevOps](../../apps/server/src/pullRequest/AzureDevOpsPullRequestProvider.ts) reads every +- [Azure DevOps](../../packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestProvider.ts) reads every version off one iteration listing, so a change too long to follow to its end leaves the paths past that point out. - [Bitbucket](../../apps/server/src/pullRequest/BitbucketPullRequestApi.ts) reads them off the diff --git a/package.json b/package.json index ed9f936dfbf9..bb1d183efb89 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "tc": "vp run -r --concurrency-limit 2 typecheck", "lint": "vp lint --report-unused-disable-directives", "knip": "knip --preprocessor ./scripts/knip-schemas.ts", - "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-acp --workspace packages/provider-acp-registry --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --workspace packages/provider-cursor --workspace packages/provider-grok --workspace packages/source-control-core --workspace packages/source-control-forgejo --workspace packages/source-control-testing --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", + "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-acp --workspace packages/provider-acp-registry --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --workspace packages/provider-cursor --workspace packages/provider-grok --workspace packages/source-control-core --workspace packages/source-control-azure-devops --workspace packages/source-control-forgejo --workspace packages/source-control-testing --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", "knip:production": "knip --production --preprocessor ./scripts/knip-schemas.ts", "lint:mobile": "node scripts/mobile-native-static-check.ts", "test": "vp run -r test", diff --git a/packages/source-control-azure-devops/package.json b/packages/source-control-azure-devops/package.json new file mode 100644 index 000000000000..10347850bf19 --- /dev/null +++ b/packages/source-control-azure-devops/package.json @@ -0,0 +1,57 @@ +{ + "name": "@t3tools/source-control-azure-devops", + "private": true, + "type": "module", + "exports": { + "./server/AzureDevOpsCli": { + "types": "./src/server/AzureDevOpsCli.ts", + "import": "./src/server/AzureDevOpsCli.ts" + }, + "./server/azureDevOpsDiff": { + "types": "./src/server/azureDevOpsDiff.ts", + "import": "./src/server/azureDevOpsDiff.ts" + }, + "./server/AzureDevOpsPullRequestCli": { + "types": "./src/server/AzureDevOpsPullRequestCli.ts", + "import": "./src/server/AzureDevOpsPullRequestCli.ts" + }, + "./server/azureDevOpsPullRequestJson": { + "types": "./src/server/azureDevOpsPullRequestJson.ts", + "import": "./src/server/azureDevOpsPullRequestJson.ts" + }, + "./server/AzureDevOpsPullRequestProvider": { + "types": "./src/server/AzureDevOpsPullRequestProvider.ts", + "import": "./src/server/AzureDevOpsPullRequestProvider.ts" + }, + "./server/azureDevOpsPullRequests": { + "types": "./src/server/azureDevOpsPullRequests.ts", + "import": "./src/server/azureDevOpsPullRequests.ts" + }, + "./server/AzureDevOpsSourceControlProvider": { + "types": "./src/server/AzureDevOpsSourceControlProvider.ts", + "import": "./src/server/AzureDevOpsSourceControlProvider.ts" + }, + "./server/driver": { + "types": "./src/server/driver.ts", + "import": "./src/server/driver.ts" + } + }, + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vp test run --config ../../vite.config.ts --dir ." + }, + "dependencies": { + "@t3tools/contracts": "workspace:*", + "@t3tools/shared": "workspace:*", + "@t3tools/source-control-core": "workspace:*", + "diff": "8.0.3", + "effect": "catalog:" + }, + "devDependencies": { + "@effect/platform-node": "catalog:", + "@effect/vitest": "catalog:", + "@t3tools/source-control-testing": "workspace:*", + "@types/node": "catalog:", + "vite-plus": "catalog:" + } +} diff --git a/apps/server/src/sourceControl/AzureDevOpsCli.test.ts b/packages/source-control-azure-devops/src/server/AzureDevOpsCli.test.ts similarity index 97% rename from apps/server/src/sourceControl/AzureDevOpsCli.test.ts rename to packages/source-control-azure-devops/src/server/AzureDevOpsCli.test.ts index 249397824609..e4fca9c5ffae 100644 --- a/apps/server/src/sourceControl/AzureDevOpsCli.test.ts +++ b/packages/source-control-azure-devops/src/server/AzureDevOpsCli.test.ts @@ -8,10 +8,11 @@ import * as PlatformError from "effect/PlatformError"; import { ChildProcessSpawner } from "effect/process"; import { VcsProcessExitError, VcsProcessSpawnError } from "@t3tools/contracts"; -import * as VcsProcess from "../vcs/VcsProcess.ts"; +import type * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; +import * as TestSourceControlHost from "@t3tools/source-control-testing/TestSourceControlHost"; import * as AzureDevOpsCli from "./AzureDevOpsCli.ts"; -const processOutput = (stdout: string): VcsProcess.VcsProcessOutput => ({ +const processOutput = (stdout: string): SourceControlHost.SourceControlProcessOutput => ({ exitCode: ChildProcessSpawner.ExitCode(0), stdout, stderr: "", @@ -19,12 +20,10 @@ const processOutput = (stdout: string): VcsProcess.VcsProcessOutput => ({ stderrTruncated: false, }); -const mockRun = vi.fn(); +const mockRun = vi.fn(); const layerSupport = Layer.mergeAll( - Layer.mock(VcsProcess.VcsProcess)({ - run: mockRun, - }), + TestSourceControlHost.layer({ process: { run: mockRun } }), NodeServices.layer, ); const layer = Layer.mergeAll(AzureDevOpsCli.layer.pipe(Layer.provide(layerSupport)), layerSupport); diff --git a/apps/server/src/sourceControl/AzureDevOpsCli.ts b/packages/source-control-azure-devops/src/server/AzureDevOpsCli.ts similarity index 98% rename from apps/server/src/sourceControl/AzureDevOpsCli.ts rename to packages/source-control-azure-devops/src/server/AzureDevOpsCli.ts index 7fd44de3c40b..c333a220c6d3 100644 --- a/apps/server/src/sourceControl/AzureDevOpsCli.ts +++ b/packages/source-control-azure-devops/src/server/AzureDevOpsCli.ts @@ -11,7 +11,7 @@ import { type VcsError, } from "@t3tools/contracts"; -import * as VcsProcess from "../vcs/VcsProcess.ts"; +import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; import { decodeAzureDevOpsPullRequestJson, decodeAzureDevOpsPullRequestListJson, @@ -217,7 +217,7 @@ export class AzureDevOpsCli extends Context.Service< readonly args: ReadonlyArray; readonly timeoutMs?: number; readonly maxOutputBytes?: number; - }) => Effect.Effect; + }) => Effect.Effect; readonly listPullRequests: (input: { readonly cwd: string; @@ -263,7 +263,7 @@ export class AzureDevOpsCli extends Context.Service< readonly remoteName?: string; }) => Effect.Effect; } ->()("t3/sourceControl/AzureDevOpsCli") {} +>()("@t3tools/source-control-azure-devops/server/AzureDevOpsCli") {} function normalizeChangeRequestId(reference: string): string { const trimmed = reference.trim().replace(/^#/, ""); @@ -352,7 +352,7 @@ function decodeAzureDevOpsJson( /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { - const process = yield* VcsProcess.VcsProcess; + const { process } = yield* SourceControlHost.SourceControlHost; const execute: AzureDevOpsCli["Service"]["execute"] = (input) => process diff --git a/apps/server/src/pullRequest/AzureDevOpsPullRequestCli.test.ts b/packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestCli.test.ts similarity index 99% rename from apps/server/src/pullRequest/AzureDevOpsPullRequestCli.test.ts rename to packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestCli.test.ts index be8354c6444c..b3afaca03715 100644 --- a/apps/server/src/pullRequest/AzureDevOpsPullRequestCli.test.ts +++ b/packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestCli.test.ts @@ -3,7 +3,7 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import { ChildProcessSpawner } from "effect/process"; -import * as AzureDevOpsCli from "../sourceControl/AzureDevOpsCli.ts"; +import * as AzureDevOpsCli from "./AzureDevOpsCli.ts"; import * as AzureDevOpsPullRequestCli from "./AzureDevOpsPullRequestCli.ts"; import * as AzureDevOpsPullRequestProvider from "./AzureDevOpsPullRequestProvider.ts"; diff --git a/apps/server/src/pullRequest/AzureDevOpsPullRequestCli.ts b/packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestCli.ts similarity index 99% rename from apps/server/src/pullRequest/AzureDevOpsPullRequestCli.ts rename to packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestCli.ts index d00d08b7d05f..70fe78b34bf9 100644 --- a/apps/server/src/pullRequest/AzureDevOpsPullRequestCli.ts +++ b/packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestCli.ts @@ -11,7 +11,7 @@ import type { PullRequestMergeMethod, } from "@t3tools/contracts"; -import * as AzureDevOpsCli from "../sourceControl/AzureDevOpsCli.ts"; +import * as AzureDevOpsCli from "./AzureDevOpsCli.ts"; import { decodeItemContentJson, decodeIterationChangesJson, @@ -258,7 +258,7 @@ export class AzureDevOpsPullRequestCli extends Context.Service< readonly requested: boolean; }) => Effect.Effect; } ->()("t3/pullRequest/AzureDevOpsPullRequestCli") {} +>()("@t3tools/source-control-azure-devops/server/AzureDevOpsPullRequestCli") {} function statusArgs(state: PullRequestListState): ReadonlyArray { switch (state) { diff --git a/apps/server/src/pullRequest/AzureDevOpsPullRequestProvider.test.ts b/packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestProvider.test.ts similarity index 100% rename from apps/server/src/pullRequest/AzureDevOpsPullRequestProvider.test.ts rename to packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestProvider.test.ts diff --git a/apps/server/src/pullRequest/AzureDevOpsPullRequestProvider.ts b/packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestProvider.ts similarity index 100% rename from apps/server/src/pullRequest/AzureDevOpsPullRequestProvider.ts rename to packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestProvider.ts diff --git a/apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.test.ts b/packages/source-control-azure-devops/src/server/AzureDevOpsSourceControlProvider.test.ts similarity index 100% rename from apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.test.ts rename to packages/source-control-azure-devops/src/server/AzureDevOpsSourceControlProvider.test.ts diff --git a/apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts b/packages/source-control-azure-devops/src/server/AzureDevOpsSourceControlProvider.ts similarity index 100% rename from apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts rename to packages/source-control-azure-devops/src/server/AzureDevOpsSourceControlProvider.ts diff --git a/apps/server/src/pullRequest/azureDevOpsDiff.test.ts b/packages/source-control-azure-devops/src/server/azureDevOpsDiff.test.ts similarity index 100% rename from apps/server/src/pullRequest/azureDevOpsDiff.test.ts rename to packages/source-control-azure-devops/src/server/azureDevOpsDiff.test.ts diff --git a/apps/server/src/pullRequest/azureDevOpsDiff.ts b/packages/source-control-azure-devops/src/server/azureDevOpsDiff.ts similarity index 100% rename from apps/server/src/pullRequest/azureDevOpsDiff.ts rename to packages/source-control-azure-devops/src/server/azureDevOpsDiff.ts diff --git a/apps/server/src/pullRequest/azureDevOpsPullRequestJson.test.ts b/packages/source-control-azure-devops/src/server/azureDevOpsPullRequestJson.test.ts similarity index 100% rename from apps/server/src/pullRequest/azureDevOpsPullRequestJson.test.ts rename to packages/source-control-azure-devops/src/server/azureDevOpsPullRequestJson.test.ts diff --git a/apps/server/src/pullRequest/azureDevOpsPullRequestJson.ts b/packages/source-control-azure-devops/src/server/azureDevOpsPullRequestJson.ts similarity index 99% rename from apps/server/src/pullRequest/azureDevOpsPullRequestJson.ts rename to packages/source-control-azure-devops/src/server/azureDevOpsPullRequestJson.ts index 2e949e103959..e5e9694f1cd3 100644 --- a/apps/server/src/pullRequest/azureDevOpsPullRequestJson.ts +++ b/packages/source-control-azure-devops/src/server/azureDevOpsPullRequestJson.ts @@ -12,7 +12,7 @@ import type { import { TrimmedNonEmptyString } from "@t3tools/contracts"; import { decodeJsonResult } from "@t3tools/shared/schemaJson"; -import { azureDevOpsPullRequestWebUrl } from "../sourceControl/azureDevOpsPullRequests.ts"; +import { azureDevOpsPullRequestWebUrl } from "./azureDevOpsPullRequests.ts"; /** * Azure's enums are decoded as plain strings and normalized here, in the same tolerant style as diff --git a/apps/server/src/sourceControl/azureDevOpsPullRequests.ts b/packages/source-control-azure-devops/src/server/azureDevOpsPullRequests.ts similarity index 100% rename from apps/server/src/sourceControl/azureDevOpsPullRequests.ts rename to packages/source-control-azure-devops/src/server/azureDevOpsPullRequests.ts diff --git a/packages/source-control-azure-devops/src/server/driver.ts b/packages/source-control-azure-devops/src/server/driver.ts new file mode 100644 index 000000000000..7a0c42c9f3ca --- /dev/null +++ b/packages/source-control-azure-devops/src/server/driver.ts @@ -0,0 +1,19 @@ +/** + * The Azure DevOps driver: repository operations, discovery, and pull requests through `az`. + * + * @module source-control-azure-devops/server/driver + */ +import { defineSourceControlDriver } from "@t3tools/source-control-core/server/driver"; +import * as Effect from "effect/Effect"; + +import * as AzureDevOpsPullRequestProvider from "./AzureDevOpsPullRequestProvider.ts"; +import * as AzureDevOpsSourceControlProvider from "./AzureDevOpsSourceControlProvider.ts"; + +export const driver = defineSourceControlDriver({ + kind: "azure-devops", + make: Effect.all({ + sourceControl: AzureDevOpsSourceControlProvider.make, + discovery: Effect.succeed(AzureDevOpsSourceControlProvider.discovery), + pullRequests: AzureDevOpsPullRequestProvider.make, + }), +}); diff --git a/packages/source-control-azure-devops/tsconfig.json b/packages/source-control-azure-devops/tsconfig.json new file mode 100644 index 000000000000..374bac55202d --- /dev/null +++ b/packages/source-control-azure-devops/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e91a9bb0555f..54036c86d862 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -648,15 +648,15 @@ importers: '@t3tools/provider-pi': specifier: workspace:* version: link:../../packages/provider-pi + '@t3tools/source-control-azure-devops': + specifier: workspace:* + version: link:../../packages/source-control-azure-devops '@t3tools/source-control-core': specifier: workspace:* version: link:../../packages/source-control-core '@t3tools/source-control-forgejo': specifier: workspace:* version: link:../../packages/source-control-forgejo - diff: - specifier: 8.0.3 - version: 8.0.3 effect: specifier: 4.0.2 version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) @@ -1516,6 +1516,40 @@ importers: specifier: 1.0.0 version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + packages/source-control-azure-devops: + dependencies: + '@t3tools/contracts': + specifier: workspace:* + version: link:../contracts + '@t3tools/shared': + specifier: workspace:* + version: link:../shared + '@t3tools/source-control-core': + specifier: workspace:* + version: link:../source-control-core + diff: + specifier: 8.0.3 + version: 8.0.3 + effect: + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) + devDependencies: + '@effect/platform-node': + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) + '@effect/vitest': + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + '@t3tools/source-control-testing': + specifier: workspace:* + version: link:../source-control-testing + '@types/node': + specifier: 24.12.4 + version: 24.12.4 + vite-plus: + specifier: 1.0.0 + version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + packages/source-control-core: dependencies: '@t3tools/contracts': diff --git a/scripts/release-smoke.ts b/scripts/release-smoke.ts index 6482dd28f295..7f29acf844a0 100644 --- a/scripts/release-smoke.ts +++ b/scripts/release-smoke.ts @@ -37,6 +37,7 @@ const workspaceFiles = [ "packages/provider-grok/package.json", "packages/provider-muse/package.json", "packages/provider-opencode/package.json", + "packages/source-control-azure-devops/package.json", "packages/source-control-core/package.json", "packages/source-control-forgejo/package.json", "packages/source-control-testing/package.json", From eb459b5afaf42621a6cb2e698715fa017e26e5d7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 14:50:01 -0700 Subject: [PATCH 13/39] refactor(source-control): GitLab lives in @t3tools/source-control-gitlab (#17594) Co-authored-by: Claude Opus 5.5 --- apps/server/package.json | 2 + apps/server/scripts/evaluate-thread-titles.ts | 2 - apps/server/src/git/GitManager.test.ts | 22 +++++--- .../PullRequestProviderRateLimit.test.ts | 4 +- .../PullRequestProviderRegistry.ts | 5 -- apps/server/src/server.ts | 2 - .../SourceControlDiscovery.test.ts | 4 +- .../SourceControlProviderRegistry.test.ts | 4 +- .../SourceControlProviderRegistry.ts | 7 --- .../src/sourceControl/builtInDrivers.ts | 6 +- apps/server/src/ws.ts | 2 - docs/internals/pull-request-file-revisions.md | 4 +- package.json | 2 +- packages/source-control-gitlab/package.json | 55 +++++++++++++++++++ .../src/server}/GitLabCli.test.ts | 15 ++--- .../src/server}/GitLabCli.ts | 8 +-- .../src/server}/GitLabPullRequestCli.test.ts | 2 +- .../src/server}/GitLabPullRequestCli.ts | 4 +- .../server}/GitLabPullRequestProvider.test.ts | 0 .../src/server}/GitLabPullRequestProvider.ts | 0 .../GitLabSourceControlProvider.test.ts | 0 .../server}/GitLabSourceControlProvider.ts | 0 .../src/server/driver.ts | 19 +++++++ .../src/server}/gitLabAuthStatus.ts | 0 .../server}/gitLabMergeRequestJson.test.ts | 0 .../src/server}/gitLabMergeRequestJson.ts | 0 .../src/server}/gitLabMergeRequests.ts | 0 packages/source-control-gitlab/tsconfig.json | 7 +++ pnpm-lock.yaml | 34 ++++++++++++ scripts/release-smoke.ts | 1 + 30 files changed, 159 insertions(+), 52 deletions(-) create mode 100644 packages/source-control-gitlab/package.json rename {apps/server/src/sourceControl => packages/source-control-gitlab/src/server}/GitLabCli.test.ts (95%) rename {apps/server/src/sourceControl => packages/source-control-gitlab/src/server}/GitLabCli.ts (98%) rename {apps/server/src/pullRequest => packages/source-control-gitlab/src/server}/GitLabPullRequestCli.test.ts (99%) rename {apps/server/src/pullRequest => packages/source-control-gitlab/src/server}/GitLabPullRequestCli.ts (99%) rename {apps/server/src/pullRequest => packages/source-control-gitlab/src/server}/GitLabPullRequestProvider.test.ts (100%) rename {apps/server/src/pullRequest => packages/source-control-gitlab/src/server}/GitLabPullRequestProvider.ts (100%) rename {apps/server/src/sourceControl => packages/source-control-gitlab/src/server}/GitLabSourceControlProvider.test.ts (100%) rename {apps/server/src/sourceControl => packages/source-control-gitlab/src/server}/GitLabSourceControlProvider.ts (100%) create mode 100644 packages/source-control-gitlab/src/server/driver.ts rename {apps/server/src/sourceControl => packages/source-control-gitlab/src/server}/gitLabAuthStatus.ts (100%) rename {apps/server/src/pullRequest => packages/source-control-gitlab/src/server}/gitLabMergeRequestJson.test.ts (100%) rename {apps/server/src/pullRequest => packages/source-control-gitlab/src/server}/gitLabMergeRequestJson.ts (100%) rename {apps/server/src/sourceControl => packages/source-control-gitlab/src/server}/gitLabMergeRequests.ts (100%) create mode 100644 packages/source-control-gitlab/tsconfig.json diff --git a/apps/server/package.json b/apps/server/package.json index 232a99960f96..a246302618a3 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -48,6 +48,7 @@ "@t3tools/source-control-azure-devops": "workspace:*", "@t3tools/source-control-core": "workspace:*", "@t3tools/source-control-forgejo": "workspace:*", + "@t3tools/source-control-gitlab": "workspace:*", "effect": "catalog:", "jose": "catalog:", "node-pty": "^1.2.0-beta.15", @@ -64,6 +65,7 @@ "@t3tools/contracts": "workspace:*", "@t3tools/provider-testing": "workspace:*", "@t3tools/shared": "workspace:*", + "@t3tools/source-control-testing": "workspace:*", "@t3tools/ssh": "workspace:*", "@t3tools/tailscale": "workspace:*", "@t3tools/web": "workspace:*", diff --git a/apps/server/scripts/evaluate-thread-titles.ts b/apps/server/scripts/evaluate-thread-titles.ts index 92313a0869c7..0b3fb1393239 100644 --- a/apps/server/scripts/evaluate-thread-titles.ts +++ b/apps/server/scripts/evaluate-thread-titles.ts @@ -24,7 +24,6 @@ import { import * as ThreadTitleLinks from "../src/textGeneration/ThreadTitleLinks.ts"; import * as SourceControlProviderRegistry from "../src/sourceControl/SourceControlProviderRegistry.ts"; import * as GitHubApi from "../src/sourceControl/GitHubApi.ts"; -import * as GitLabCli from "../src/sourceControl/GitLabCli.ts"; import * as SourceControlBuiltInDrivers from "../src/sourceControl/builtInDrivers.ts"; import * as BitbucketApi from "../src/sourceControl/BitbucketApi.ts"; import * as ServerSettings from "../src/serverSettings.ts"; @@ -152,7 +151,6 @@ await Effect.runPromise( Layer.provide( Layer.mergeAll( GitHubApi.layerWithDependencies, - GitLabCli.layer, SourceControlBuiltInDrivers.layer, // No saved credentials here; Bitbucket falls back to T3CODE_BITBUCKET_* variables. BitbucketApi.layer, diff --git a/apps/server/src/git/GitManager.test.ts b/apps/server/src/git/GitManager.test.ts index 9df266d56c8f..92b7fcd607fc 100644 --- a/apps/server/src/git/GitManager.test.ts +++ b/apps/server/src/git/GitManager.test.ts @@ -44,7 +44,9 @@ import { } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; import { decodeGitHubPullRequestListJson } from "../sourceControl/gitHubPullRequests.ts"; -import * as GitLabCli from "../sourceControl/GitLabCli.ts"; +import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli"; +import type * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; +import * as TestSourceControlHost from "@t3tools/source-control-testing/TestSourceControlHost"; import * as TextGeneration from "../textGeneration/TextGeneration.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; @@ -53,7 +55,7 @@ import * as VcsProjectConfig from "../vcs/VcsProjectConfig.ts"; import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts"; import * as BackgroundPolicy from "../background/BackgroundPolicy.ts"; import * as GitWorkflowService from "./GitWorkflowService.ts"; -import * as GitLabSourceControlProvider from "../sourceControl/GitLabSourceControlProvider.ts"; +import * as GitLabSourceControlProvider from "@t3tools/source-control-gitlab/server/GitLabSourceControlProvider"; import { ForgejoPullRequestSchema, toForgejoChangeRequest, @@ -2108,17 +2110,19 @@ it.layer(layerGitManagerTest)("GitManager", (it) => { source_project: { path_with_namespace: "Group/Subgroup/Fork" }, }, ]); - const calls: VcsProcess.VcsProcessInput[] = []; + const calls: SourceControlHost.SourceControlProcessInput[] = []; const provider = yield* GitLabSourceControlProvider.make.pipe( Effect.provide( GitLabCli.layer.pipe( Layer.provide( - Layer.mock(VcsProcess.VcsProcess)({ - run: (input) => - Effect.sync(() => { - calls.push(input); - return fakeGhOutput(output); - }), + TestSourceControlHost.layer({ + process: { + run: (input) => + Effect.sync(() => { + calls.push(input); + return fakeGhOutput(output); + }), + }, }), ), ), diff --git a/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts b/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts index 38f587353980..b26bbe8ae9ba 100644 --- a/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts +++ b/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts @@ -4,11 +4,11 @@ import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/Azu import * as BitbucketApi from "../sourceControl/BitbucketApi.ts"; import * as GitHubApi from "../sourceControl/GitHubApi.ts"; import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; -import * as GitLabCli from "../sourceControl/GitLabCli.ts"; +import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli"; import { azureDevOpsProviderFailure } from "@t3tools/source-control-azure-devops/server/AzureDevOpsPullRequestProvider"; import { bitbucketProviderFailure } from "./BitbucketPullRequestProvider.ts"; import { gitHubProviderFailure } from "./GitHubPullRequestProvider.ts"; -import { gitLabProviderFailure } from "./GitLabPullRequestProvider.ts"; +import { gitLabProviderFailure } from "@t3tools/source-control-gitlab/server/GitLabPullRequestProvider"; const cause = new Error("redacted provider failure"); diff --git a/apps/server/src/pullRequest/PullRequestProviderRegistry.ts b/apps/server/src/pullRequest/PullRequestProviderRegistry.ts index 009d3d3f8b4f..74d7dc471748 100644 --- a/apps/server/src/pullRequest/PullRequestProviderRegistry.ts +++ b/apps/server/src/pullRequest/PullRequestProviderRegistry.ts @@ -5,13 +5,10 @@ import type { SourceControlProviderKind } from "@t3tools/contracts"; import * as BitbucketApi from "../sourceControl/BitbucketApi.ts"; import * as GitHubApi from "../sourceControl/GitHubApi.ts"; -import * as GitLabCli from "../sourceControl/GitLabCli.ts"; import * as BitbucketPullRequestApi from "./BitbucketPullRequestApi.ts"; import * as BitbucketPullRequestProvider from "./BitbucketPullRequestProvider.ts"; import * as GitHubPullRequestApi from "./GitHubPullRequestApi.ts"; import * as GitHubPullRequestProvider from "./GitHubPullRequestProvider.ts"; -import * as GitLabPullRequestCli from "./GitLabPullRequestCli.ts"; -import * as GitLabPullRequestProvider from "./GitLabPullRequestProvider.ts"; import * as BuiltInDrivers from "../sourceControl/builtInDrivers.ts"; import type { PullRequestProviderApi } from "@t3tools/source-control-core/server/PullRequestProvider"; @@ -44,7 +41,6 @@ export function fromProviders( export const make = Effect.gen(function* () { const providers = yield* Effect.all([ GitHubPullRequestProvider.make, - GitLabPullRequestProvider.make, BitbucketPullRequestProvider.make, ]); const drivers = yield* Effect.forEach(BuiltInDrivers.BUILT_IN_SOURCE_CONTROL_DRIVERS, (driver) => @@ -65,7 +61,6 @@ export const layer = Layer.effect(PullRequestProviderRegistry, make).pipe( ), ), ), - Layer.provide(GitLabPullRequestCli.layer.pipe(Layer.provide(GitLabCli.layer))), Layer.provide(BuiltInDrivers.layer), Layer.provide(BitbucketPullRequestApi.layer.pipe(Layer.provide(BitbucketApi.layer))), ); diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 82c5de077ef7..8ea3b5cda198 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -54,7 +54,6 @@ import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts"; import * as CheckpointStore from "./checkpointing/CheckpointStore.ts"; import * as BitbucketApi from "./sourceControl/BitbucketApi.ts"; import * as GitHubApi from "./sourceControl/GitHubApi.ts"; -import * as GitLabCli from "./sourceControl/GitLabCli.ts"; import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; import * as SourceControlBuiltInDrivers from "./sourceControl/builtInDrivers.ts"; import * as TextGeneration from "./textGeneration/TextGeneration.ts"; @@ -302,7 +301,6 @@ const layerSourceControlProviderRegistry = SourceControlProviderRegistry.layer.p Layer.mergeAll( BitbucketApi.layer, GitHubApi.layerWithDependencies, - GitLabCli.layer, SourceControlBuiltInDrivers.layer, ), ), diff --git a/apps/server/src/sourceControl/SourceControlDiscovery.test.ts b/apps/server/src/sourceControl/SourceControlDiscovery.test.ts index e818a9274635..c87d731953da 100644 --- a/apps/server/src/sourceControl/SourceControlDiscovery.test.ts +++ b/apps/server/src/sourceControl/SourceControlDiscovery.test.ts @@ -20,7 +20,8 @@ import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/Azu import * as AzureDevOpsPullRequestCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsPullRequestCli"; import * as BitbucketApi from "./BitbucketApi.ts"; import * as GitHubApi from "./GitHubApi.ts"; -import * as GitLabCli from "./GitLabCli.ts"; +import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli"; +import * as GitLabPullRequestCli from "@t3tools/source-control-gitlab/server/GitLabPullRequestCli"; import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; import * as ForgejoSourceControlProvider from "@t3tools/source-control-forgejo/server/ForgejoSourceControlProvider"; import * as ForgejoPullRequestProvider from "@t3tools/source-control-forgejo/server/ForgejoPullRequestProvider"; @@ -46,6 +47,7 @@ const layerSourceControlProviderRegistryTest = (input: { Layer.mock(GitHubApi.GitHubApi)({}), Layer.mock(GitVcsDriver.GitVcsDriver)({}), Layer.mock(GitLabCli.GitLabCli)({}), + Layer.mock(GitLabPullRequestCli.GitLabPullRequestCli)({}), Layer.mock(ForgejoCli.ForgejoCli)({ listLogins: () => Effect.succeed([]) }), Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({}), Layer.mock(VcsProcess.VcsProcess)(input.process), diff --git a/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts b/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts index 5f48bae18588..582848b38a4b 100644 --- a/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts +++ b/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts @@ -19,7 +19,8 @@ import * as BitbucketApi from "./BitbucketApi.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as GitHubApi from "./GitHubApi.ts"; import * as GitHubCredentials from "./GitHubCredentials.ts"; -import * as GitLabCli from "./GitLabCli.ts"; +import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli"; +import * as GitLabPullRequestCli from "@t3tools/source-control-gitlab/server/GitLabPullRequestCli"; import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; import * as ServerSourceControlHost from "./ServerSourceControlHost.ts"; @@ -114,6 +115,7 @@ function makeRegistry(input: { }), Layer.mock(GitVcsDriver.GitVcsDriver)({}), Layer.mock(GitLabCli.GitLabCli)(input.gitlab ?? {}), + Layer.mock(GitLabPullRequestCli.GitLabPullRequestCli)({}), Layer.mock(ForgejoCli.ForgejoCli)({ listLogins: () => Effect.succeed([]) }), ServerConfig.layerTest(process.cwd(), { prefix: "t3-source-control-registry-test-", diff --git a/apps/server/src/sourceControl/SourceControlProviderRegistry.ts b/apps/server/src/sourceControl/SourceControlProviderRegistry.ts index 948d583296fa..9eeae3d935f7 100644 --- a/apps/server/src/sourceControl/SourceControlProviderRegistry.ts +++ b/apps/server/src/sourceControl/SourceControlProviderRegistry.ts @@ -13,7 +13,6 @@ import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/source import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; -import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts"; import * as BuiltInDrivers from "./builtInDrivers.ts"; import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; import { @@ -306,7 +305,6 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit export const make = Effect.gen(function* () { const github = yield* GitHubSourceControlProvider.make; const githubDiscovery = yield* GitHubSourceControlProvider.makeDiscovery; - const gitlab = yield* GitLabSourceControlProvider.make; const bitbucket = yield* BitbucketSourceControlProvider.make; const bitbucketDiscovery = yield* BitbucketSourceControlProvider.makeDiscovery; const drivers = yield* Effect.forEach(BuiltInDrivers.BUILT_IN_SOURCE_CONTROL_DRIVERS, (driver) => @@ -326,11 +324,6 @@ export const make = Effect.gen(function* () { provider: github, discovery: githubDiscovery, }, - { - kind: "gitlab", - provider: gitlab, - discovery: GitLabSourceControlProvider.discovery, - }, { kind: "bitbucket", provider: bitbucket, diff --git a/apps/server/src/sourceControl/builtInDrivers.ts b/apps/server/src/sourceControl/builtInDrivers.ts index 584bf139c0fb..83ee1d35e8f5 100644 --- a/apps/server/src/sourceControl/builtInDrivers.ts +++ b/apps/server/src/sourceControl/builtInDrivers.ts @@ -12,12 +12,15 @@ import * as AzureDevOpsPullRequestCli from "@t3tools/source-control-azure-devops import * as AzureDevOpsDriver from "@t3tools/source-control-azure-devops/server/driver"; import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; import * as ForgejoDriver from "@t3tools/source-control-forgejo/server/driver"; +import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli"; +import * as GitLabPullRequestCli from "@t3tools/source-control-gitlab/server/GitLabPullRequestCli"; +import * as GitLabDriver from "@t3tools/source-control-gitlab/server/driver"; import type { SourceControlDriver } from "@t3tools/source-control-core/server/driver"; import * as Layer from "effect/Layer"; import * as ServerSourceControlHost from "./ServerSourceControlHost.ts"; -const drivers = [AzureDevOpsDriver.driver, ForgejoDriver.driver]; +const drivers = [GitLabDriver.driver, AzureDevOpsDriver.driver, ForgejoDriver.driver]; /** Every service a built-in driver's `make` needs; the server's layers must provide them all. */ export type BuiltInSourceControlDriversEnv = @@ -31,4 +34,5 @@ export const BUILT_IN_SOURCE_CONTROL_DRIVERS: ReadonlyArray< export const layer = Layer.mergeAll( AzureDevOpsPullRequestCli.layer.pipe(Layer.provideMerge(AzureDevOpsCli.layer)), ForgejoCli.layer, + GitLabPullRequestCli.layer.pipe(Layer.provideMerge(GitLabCli.layer)), ).pipe(Layer.provideMerge(ServerSourceControlHost.layer)); diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 899f021eb937..19ed2600ba5e 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -231,7 +231,6 @@ import * as SourceControlDiscovery from "./sourceControl/SourceControlDiscovery. import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts"; import * as BitbucketApi from "./sourceControl/BitbucketApi.ts"; import * as GitHubApi from "./sourceControl/GitHubApi.ts"; -import * as GitLabCli from "./sourceControl/GitLabCli.ts"; import * as SourceControlBuiltInDrivers from "./sourceControl/builtInDrivers.ts"; import * as SourceControlProviderRegistry from "./sourceControl/SourceControlProviderRegistry.ts"; import * as GitVcsDriver from "./vcs/GitVcsDriver.ts"; @@ -3205,7 +3204,6 @@ export const layer = Layer.unwrap( Layer.mergeAll( BitbucketApi.layer, GitHubApi.layerWithDependencies, - GitLabCli.layer, SourceControlBuiltInDrivers.layer, ), ), diff --git a/docs/internals/pull-request-file-revisions.md b/docs/internals/pull-request-file-revisions.md index ceb80de8efe0..8fd0dfdb69db 100644 --- a/docs/internals/pull-request-file-revisions.md +++ b/docs/internals/pull-request-file-revisions.md @@ -7,7 +7,7 @@ one. No type distinguishes those meanings and no single file states the whole pa this page is for. The failure is on record rather than hypothetical. The interface doc above -[GitLab's `getFileRevisions`](../../apps/server/src/pullRequest/GitLabPullRequestCli.ts) once said +[GitLab's `getFileRevisions`](../../packages/source-control-gitlab/src/server/GitLabPullRequestCli.ts) once said the opposite of the code beneath it, because it described a missing path the way the layer below means it. Anything written against that reading would treat a version the host declined to give as a file the change request deleted, which reports every file a reader has cleared as changed across @@ -30,7 +30,7 @@ What "never got to look at" is belongs to the host, and the contract cannot know - [Bitbucket](../../apps/server/src/pullRequest/BitbucketPullRequestApi.ts) reads them off the pull request's own patch, the only place it states a file's version, so a patch cut short at the byte ceiling leaves the paths past the cut out. -- [GitLab](../../apps/server/src/pullRequest/GitLabPullRequestCli.ts) asks in batches, so a batch +- [GitLab](../../packages/source-control-gitlab/src/server/GitLabPullRequestCli.ts) asks in batches, so a batch it could not read leaves that batch's paths out. Those three facts stay with their providers. The contract states only that absence means the read diff --git a/package.json b/package.json index bb1d183efb89..4e0ba35ecc7e 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "tc": "vp run -r --concurrency-limit 2 typecheck", "lint": "vp lint --report-unused-disable-directives", "knip": "knip --preprocessor ./scripts/knip-schemas.ts", - "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-acp --workspace packages/provider-acp-registry --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --workspace packages/provider-cursor --workspace packages/provider-grok --workspace packages/source-control-core --workspace packages/source-control-azure-devops --workspace packages/source-control-forgejo --workspace packages/source-control-testing --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", + "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-acp --workspace packages/provider-acp-registry --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --workspace packages/provider-cursor --workspace packages/provider-grok --workspace packages/source-control-core --workspace packages/source-control-azure-devops --workspace packages/source-control-forgejo --workspace packages/source-control-gitlab --workspace packages/source-control-testing --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", "knip:production": "knip --production --preprocessor ./scripts/knip-schemas.ts", "lint:mobile": "node scripts/mobile-native-static-check.ts", "test": "vp run -r test", diff --git a/packages/source-control-gitlab/package.json b/packages/source-control-gitlab/package.json new file mode 100644 index 000000000000..6b99c878ccb8 --- /dev/null +++ b/packages/source-control-gitlab/package.json @@ -0,0 +1,55 @@ +{ + "name": "@t3tools/source-control-gitlab", + "private": true, + "type": "module", + "exports": { + "./server/driver": { + "types": "./src/server/driver.ts", + "import": "./src/server/driver.ts" + }, + "./server/gitLabAuthStatus": { + "types": "./src/server/gitLabAuthStatus.ts", + "import": "./src/server/gitLabAuthStatus.ts" + }, + "./server/GitLabCli": { + "types": "./src/server/GitLabCli.ts", + "import": "./src/server/GitLabCli.ts" + }, + "./server/gitLabMergeRequestJson": { + "types": "./src/server/gitLabMergeRequestJson.ts", + "import": "./src/server/gitLabMergeRequestJson.ts" + }, + "./server/gitLabMergeRequests": { + "types": "./src/server/gitLabMergeRequests.ts", + "import": "./src/server/gitLabMergeRequests.ts" + }, + "./server/GitLabPullRequestCli": { + "types": "./src/server/GitLabPullRequestCli.ts", + "import": "./src/server/GitLabPullRequestCli.ts" + }, + "./server/GitLabPullRequestProvider": { + "types": "./src/server/GitLabPullRequestProvider.ts", + "import": "./src/server/GitLabPullRequestProvider.ts" + }, + "./server/GitLabSourceControlProvider": { + "types": "./src/server/GitLabSourceControlProvider.ts", + "import": "./src/server/GitLabSourceControlProvider.ts" + } + }, + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vp test run --config ../../vite.config.ts --dir ." + }, + "dependencies": { + "@t3tools/contracts": "workspace:*", + "@t3tools/shared": "workspace:*", + "@t3tools/source-control-core": "workspace:*", + "effect": "catalog:" + }, + "devDependencies": { + "@effect/vitest": "catalog:", + "@t3tools/source-control-testing": "workspace:*", + "@types/node": "catalog:", + "vite-plus": "catalog:" + } +} diff --git a/apps/server/src/sourceControl/GitLabCli.test.ts b/packages/source-control-gitlab/src/server/GitLabCli.test.ts similarity index 95% rename from apps/server/src/sourceControl/GitLabCli.test.ts rename to packages/source-control-gitlab/src/server/GitLabCli.test.ts index 5a10f13b8f8f..2756460f78f5 100644 --- a/apps/server/src/sourceControl/GitLabCli.test.ts +++ b/packages/source-control-gitlab/src/server/GitLabCli.test.ts @@ -5,21 +5,16 @@ import { ChildProcessSpawner } from "effect/process"; import { VcsProcessExitError } from "@t3tools/contracts"; -import * as VcsProcess from "../vcs/VcsProcess.ts"; +import type * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; +import * as TestSourceControlHost from "@t3tools/source-control-testing/TestSourceControlHost"; import * as GitLabCli from "./GitLabCli.ts"; -const mockedRun = vi.fn(); +const mockedRun = vi.fn(); const layer = it.layer( - GitLabCli.layer.pipe( - Layer.provide( - Layer.mock(VcsProcess.VcsProcess)({ - run: mockedRun, - }), - ), - ), + GitLabCli.layer.pipe(Layer.provide(TestSourceControlHost.layer({ process: { run: mockedRun } }))), ); -function processOutput(stdout: string): VcsProcess.VcsProcessOutput { +function processOutput(stdout: string): SourceControlHost.SourceControlProcessOutput { return { exitCode: ChildProcessSpawner.ExitCode(0), stdout, diff --git a/apps/server/src/sourceControl/GitLabCli.ts b/packages/source-control-gitlab/src/server/GitLabCli.ts similarity index 98% rename from apps/server/src/sourceControl/GitLabCli.ts rename to packages/source-control-gitlab/src/server/GitLabCli.ts index 2ec69a527fed..6845ee771001 100644 --- a/apps/server/src/sourceControl/GitLabCli.ts +++ b/packages/source-control-gitlab/src/server/GitLabCli.ts @@ -13,7 +13,7 @@ import { type VcsError, } from "@t3tools/contracts"; -import * as VcsProcess from "../vcs/VcsProcess.ts"; +import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; import { decodeGitLabMergeRequestJson, decodeGitLabMergeRequestListJson, @@ -270,7 +270,7 @@ export class GitLabCli extends Context.Service< /** Piped to the child's stdin, for payloads that must never appear in argv. */ readonly stdin?: string; readonly maxOutputBytes?: number; - }) => Effect.Effect; + }) => Effect.Effect; readonly listMergeRequests: (input: { readonly cwd: string; @@ -316,7 +316,7 @@ export class GitLabCli extends Context.Service< readonly force?: boolean; }) => Effect.Effect; } ->()("t3/sourceControl/GitLabCli") {} +>()("@t3tools/source-control-gitlab/server/GitLabCli") {} const RawGitLabRepositoryCloneUrlsSchema = Schema.Struct({ path_with_namespace: TrimmedNonEmptyString, @@ -410,7 +410,7 @@ function parseRepositoryPath(repository: string): { /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { - const process = yield* VcsProcess.VcsProcess; + const { process } = yield* SourceControlHost.SourceControlHost; const run = ( input: Parameters[0], diff --git a/apps/server/src/pullRequest/GitLabPullRequestCli.test.ts b/packages/source-control-gitlab/src/server/GitLabPullRequestCli.test.ts similarity index 99% rename from apps/server/src/pullRequest/GitLabPullRequestCli.test.ts rename to packages/source-control-gitlab/src/server/GitLabPullRequestCli.test.ts index 8fb1334de364..b4546162f30c 100644 --- a/apps/server/src/pullRequest/GitLabPullRequestCli.test.ts +++ b/packages/source-control-gitlab/src/server/GitLabPullRequestCli.test.ts @@ -3,7 +3,7 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import { ChildProcessSpawner } from "effect/process"; -import * as GitLabCli from "../sourceControl/GitLabCli.ts"; +import * as GitLabCli from "./GitLabCli.ts"; import * as GitLabPullRequestCli from "./GitLabPullRequestCli.ts"; const mockedExecute = vi.fn(); diff --git a/apps/server/src/pullRequest/GitLabPullRequestCli.ts b/packages/source-control-gitlab/src/server/GitLabPullRequestCli.ts similarity index 99% rename from apps/server/src/pullRequest/GitLabPullRequestCli.ts rename to packages/source-control-gitlab/src/server/GitLabPullRequestCli.ts index 933e68742187..643d130c85bc 100644 --- a/apps/server/src/pullRequest/GitLabPullRequestCli.ts +++ b/packages/source-control-gitlab/src/server/GitLabPullRequestCli.ts @@ -20,7 +20,7 @@ import type { PullRequestReviewerCandidateList, } from "@t3tools/contracts"; -import * as GitLabCli from "../sourceControl/GitLabCli.ts"; +import * as GitLabCli from "./GitLabCli.ts"; import { AWARD_EMOJI_GRAPHQL_QUERY, decodeAwardEmojiJson, @@ -411,7 +411,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly reacted: boolean; }) => Effect.Effect; } ->()("t3/pullRequest/GitLabPullRequestCli") {} +>()("@t3tools/source-control-gitlab/server/GitLabPullRequestCli") {} /** The REST API addresses a project by its URL-encoded full path. */ function projectPath(repository: string): string { diff --git a/apps/server/src/pullRequest/GitLabPullRequestProvider.test.ts b/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.test.ts similarity index 100% rename from apps/server/src/pullRequest/GitLabPullRequestProvider.test.ts rename to packages/source-control-gitlab/src/server/GitLabPullRequestProvider.test.ts diff --git a/apps/server/src/pullRequest/GitLabPullRequestProvider.ts b/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.ts similarity index 100% rename from apps/server/src/pullRequest/GitLabPullRequestProvider.ts rename to packages/source-control-gitlab/src/server/GitLabPullRequestProvider.ts diff --git a/apps/server/src/sourceControl/GitLabSourceControlProvider.test.ts b/packages/source-control-gitlab/src/server/GitLabSourceControlProvider.test.ts similarity index 100% rename from apps/server/src/sourceControl/GitLabSourceControlProvider.test.ts rename to packages/source-control-gitlab/src/server/GitLabSourceControlProvider.test.ts diff --git a/apps/server/src/sourceControl/GitLabSourceControlProvider.ts b/packages/source-control-gitlab/src/server/GitLabSourceControlProvider.ts similarity index 100% rename from apps/server/src/sourceControl/GitLabSourceControlProvider.ts rename to packages/source-control-gitlab/src/server/GitLabSourceControlProvider.ts diff --git a/packages/source-control-gitlab/src/server/driver.ts b/packages/source-control-gitlab/src/server/driver.ts new file mode 100644 index 000000000000..29327a35019b --- /dev/null +++ b/packages/source-control-gitlab/src/server/driver.ts @@ -0,0 +1,19 @@ +/** + * The GitLab driver: repository operations, discovery, and merge requests through `glab`. + * + * @module source-control-gitlab/server/driver + */ +import { defineSourceControlDriver } from "@t3tools/source-control-core/server/driver"; +import * as Effect from "effect/Effect"; + +import * as GitLabPullRequestProvider from "./GitLabPullRequestProvider.ts"; +import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts"; + +export const driver = defineSourceControlDriver({ + kind: "gitlab", + make: Effect.all({ + sourceControl: GitLabSourceControlProvider.make, + discovery: Effect.succeed(GitLabSourceControlProvider.discovery), + pullRequests: GitLabPullRequestProvider.make, + }), +}); diff --git a/apps/server/src/sourceControl/gitLabAuthStatus.ts b/packages/source-control-gitlab/src/server/gitLabAuthStatus.ts similarity index 100% rename from apps/server/src/sourceControl/gitLabAuthStatus.ts rename to packages/source-control-gitlab/src/server/gitLabAuthStatus.ts diff --git a/apps/server/src/pullRequest/gitLabMergeRequestJson.test.ts b/packages/source-control-gitlab/src/server/gitLabMergeRequestJson.test.ts similarity index 100% rename from apps/server/src/pullRequest/gitLabMergeRequestJson.test.ts rename to packages/source-control-gitlab/src/server/gitLabMergeRequestJson.test.ts diff --git a/apps/server/src/pullRequest/gitLabMergeRequestJson.ts b/packages/source-control-gitlab/src/server/gitLabMergeRequestJson.ts similarity index 100% rename from apps/server/src/pullRequest/gitLabMergeRequestJson.ts rename to packages/source-control-gitlab/src/server/gitLabMergeRequestJson.ts diff --git a/apps/server/src/sourceControl/gitLabMergeRequests.ts b/packages/source-control-gitlab/src/server/gitLabMergeRequests.ts similarity index 100% rename from apps/server/src/sourceControl/gitLabMergeRequests.ts rename to packages/source-control-gitlab/src/server/gitLabMergeRequests.ts diff --git a/packages/source-control-gitlab/tsconfig.json b/packages/source-control-gitlab/tsconfig.json new file mode 100644 index 000000000000..374bac55202d --- /dev/null +++ b/packages/source-control-gitlab/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54036c86d862..13c583be13f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -657,6 +657,9 @@ importers: '@t3tools/source-control-forgejo': specifier: workspace:* version: link:../../packages/source-control-forgejo + '@t3tools/source-control-gitlab': + specifier: workspace:* + version: link:../../packages/source-control-gitlab effect: specifier: 4.0.2 version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) @@ -700,6 +703,9 @@ importers: '@t3tools/shared': specifier: workspace:* version: link:../../packages/shared + '@t3tools/source-control-testing': + specifier: workspace:* + version: link:../../packages/source-control-testing '@t3tools/ssh': specifier: workspace:* version: link:../../packages/ssh @@ -1603,6 +1609,34 @@ importers: specifier: 1.0.0 version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + packages/source-control-gitlab: + dependencies: + '@t3tools/contracts': + specifier: workspace:* + version: link:../contracts + '@t3tools/shared': + specifier: workspace:* + version: link:../shared + '@t3tools/source-control-core': + specifier: workspace:* + version: link:../source-control-core + effect: + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) + devDependencies: + '@effect/vitest': + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + '@t3tools/source-control-testing': + specifier: workspace:* + version: link:../source-control-testing + '@types/node': + specifier: 24.12.4 + version: 24.12.4 + vite-plus: + specifier: 1.0.0 + version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + packages/source-control-testing: dependencies: '@t3tools/contracts': diff --git a/scripts/release-smoke.ts b/scripts/release-smoke.ts index 7f29acf844a0..c19a1ee9cac4 100644 --- a/scripts/release-smoke.ts +++ b/scripts/release-smoke.ts @@ -40,6 +40,7 @@ const workspaceFiles = [ "packages/source-control-azure-devops/package.json", "packages/source-control-core/package.json", "packages/source-control-forgejo/package.json", + "packages/source-control-gitlab/package.json", "packages/source-control-testing/package.json", "packages/effect-acp/package.json", "packages/effect-codex-app-server/package.json", From 8d1858d9d608d78b67268cdc52bceb4302ae8870 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 14:50:01 -0700 Subject: [PATCH 14/39] refactor(source-control): Bitbucket lives in @t3tools/source-control-bitbucket (#17597) Co-authored-by: Claude Opus 5.5 --- apps/server/package.json | 1 + apps/server/scripts/evaluate-thread-titles.ts | 8 +-- .../PullRequestProviderRateLimit.test.ts | 4 +- .../PullRequestProviderRegistry.ts | 9 +--- apps/server/src/server.ts | 9 +--- .../src/sourceControl/BitbucketApi.test.ts | 6 ++- .../sourceControl/ServerSourceControlHost.ts | 16 ++++++ .../SourceControlDiscovery.test.ts | 11 +++- .../SourceControlProviderRegistry.test.ts | 11 +++- .../SourceControlProviderRegistry.ts | 8 --- .../SourceControlRepositoryService.test.ts | 4 +- .../SourceControlRepositoryService.ts | 2 +- .../src/sourceControl/builtInDrivers.ts | 11 +++- apps/server/src/ws.ts | 2 - docs/internals/pull-request-file-revisions.md | 2 +- package.json | 2 +- .../source-control-bitbucket/package.json | 51 ++++++++++++++++++ .../src/server}/BitbucketApi.ts | 17 +++--- .../server}/BitbucketPullRequestApi.test.ts | 2 +- .../src/server}/BitbucketPullRequestApi.ts | 4 +- .../BitbucketPullRequestProvider.test.ts | 2 +- .../server}/BitbucketPullRequestProvider.ts | 0 .../BitbucketSourceControlProvider.test.ts | 0 .../server}/BitbucketSourceControlProvider.ts | 0 .../server}/bitbucketPullRequestJson.test.ts | 0 .../src/server}/bitbucketPullRequestJson.ts | 0 .../src/server}/bitbucketPullRequests.ts | 0 .../src/server/driver.ts | 20 +++++++ .../source-control-bitbucket/tsconfig.json | 7 +++ .../src/server/SourceControlHost.ts | 52 ++++++++++++++++++- .../src/TestSourceControlHost.ts | 23 +++++++- pnpm-lock.yaml | 31 +++++++++++ scripts/release-smoke.ts | 1 + 33 files changed, 253 insertions(+), 63 deletions(-) create mode 100644 packages/source-control-bitbucket/package.json rename {apps/server/src/sourceControl => packages/source-control-bitbucket/src/server}/BitbucketApi.ts (98%) rename {apps/server/src/pullRequest => packages/source-control-bitbucket/src/server}/BitbucketPullRequestApi.test.ts (99%) rename {apps/server/src/pullRequest => packages/source-control-bitbucket/src/server}/BitbucketPullRequestApi.ts (99%) rename {apps/server/src/pullRequest => packages/source-control-bitbucket/src/server}/BitbucketPullRequestProvider.test.ts (99%) rename {apps/server/src/pullRequest => packages/source-control-bitbucket/src/server}/BitbucketPullRequestProvider.ts (100%) rename {apps/server/src/sourceControl => packages/source-control-bitbucket/src/server}/BitbucketSourceControlProvider.test.ts (100%) rename {apps/server/src/sourceControl => packages/source-control-bitbucket/src/server}/BitbucketSourceControlProvider.ts (100%) rename {apps/server/src/pullRequest => packages/source-control-bitbucket/src/server}/bitbucketPullRequestJson.test.ts (100%) rename {apps/server/src/pullRequest => packages/source-control-bitbucket/src/server}/bitbucketPullRequestJson.ts (100%) rename {apps/server/src/sourceControl => packages/source-control-bitbucket/src/server}/bitbucketPullRequests.ts (100%) create mode 100644 packages/source-control-bitbucket/src/server/driver.ts create mode 100644 packages/source-control-bitbucket/tsconfig.json diff --git a/apps/server/package.json b/apps/server/package.json index a246302618a3..ab9ad49d4e62 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -46,6 +46,7 @@ "@t3tools/provider-opencode": "workspace:*", "@t3tools/provider-pi": "workspace:*", "@t3tools/source-control-azure-devops": "workspace:*", + "@t3tools/source-control-bitbucket": "workspace:*", "@t3tools/source-control-core": "workspace:*", "@t3tools/source-control-forgejo": "workspace:*", "@t3tools/source-control-gitlab": "workspace:*", diff --git a/apps/server/scripts/evaluate-thread-titles.ts b/apps/server/scripts/evaluate-thread-titles.ts index 0b3fb1393239..598f6cc2f930 100644 --- a/apps/server/scripts/evaluate-thread-titles.ts +++ b/apps/server/scripts/evaluate-thread-titles.ts @@ -25,7 +25,6 @@ import * as ThreadTitleLinks from "../src/textGeneration/ThreadTitleLinks.ts"; import * as SourceControlProviderRegistry from "../src/sourceControl/SourceControlProviderRegistry.ts"; import * as GitHubApi from "../src/sourceControl/GitHubApi.ts"; import * as SourceControlBuiltInDrivers from "../src/sourceControl/builtInDrivers.ts"; -import * as BitbucketApi from "../src/sourceControl/BitbucketApi.ts"; import * as ServerSettings from "../src/serverSettings.ts"; import * as VcsProcess from "../src/vcs/VcsProcess.ts"; import * as VcsDriverRegistry from "../src/vcs/VcsDriverRegistry.ts"; @@ -149,12 +148,7 @@ await Effect.runPromise( ProcessRunner.layer, SourceControlProviderRegistry.layer.pipe( Layer.provide( - Layer.mergeAll( - GitHubApi.layerWithDependencies, - SourceControlBuiltInDrivers.layer, - // No saved credentials here; Bitbucket falls back to T3CODE_BITBUCKET_* variables. - BitbucketApi.layer, - ), + Layer.mergeAll(GitHubApi.layerWithDependencies, SourceControlBuiltInDrivers.layer), ), // Default settings: no saved Bitbucket token, gh's own GitHub account choice. Layer.provide(ServerSettings.layerTest()), diff --git a/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts b/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts index b26bbe8ae9ba..6d35adc07246 100644 --- a/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts +++ b/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts @@ -1,12 +1,12 @@ import { assert, it } from "@effect/vitest"; import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsCli"; -import * as BitbucketApi from "../sourceControl/BitbucketApi.ts"; +import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; import * as GitHubApi from "../sourceControl/GitHubApi.ts"; import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli"; import { azureDevOpsProviderFailure } from "@t3tools/source-control-azure-devops/server/AzureDevOpsPullRequestProvider"; -import { bitbucketProviderFailure } from "./BitbucketPullRequestProvider.ts"; +import { bitbucketProviderFailure } from "@t3tools/source-control-bitbucket/server/BitbucketPullRequestProvider"; import { gitHubProviderFailure } from "./GitHubPullRequestProvider.ts"; import { gitLabProviderFailure } from "@t3tools/source-control-gitlab/server/GitLabPullRequestProvider"; diff --git a/apps/server/src/pullRequest/PullRequestProviderRegistry.ts b/apps/server/src/pullRequest/PullRequestProviderRegistry.ts index 74d7dc471748..5702957a0ff4 100644 --- a/apps/server/src/pullRequest/PullRequestProviderRegistry.ts +++ b/apps/server/src/pullRequest/PullRequestProviderRegistry.ts @@ -3,10 +3,7 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import type { SourceControlProviderKind } from "@t3tools/contracts"; -import * as BitbucketApi from "../sourceControl/BitbucketApi.ts"; import * as GitHubApi from "../sourceControl/GitHubApi.ts"; -import * as BitbucketPullRequestApi from "./BitbucketPullRequestApi.ts"; -import * as BitbucketPullRequestProvider from "./BitbucketPullRequestProvider.ts"; import * as GitHubPullRequestApi from "./GitHubPullRequestApi.ts"; import * as GitHubPullRequestProvider from "./GitHubPullRequestProvider.ts"; import * as BuiltInDrivers from "../sourceControl/builtInDrivers.ts"; @@ -39,10 +36,7 @@ export function fromProviders( * @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { - const providers = yield* Effect.all([ - GitHubPullRequestProvider.make, - BitbucketPullRequestProvider.make, - ]); + const providers = yield* Effect.all([GitHubPullRequestProvider.make]); const drivers = yield* Effect.forEach(BuiltInDrivers.BUILT_IN_SOURCE_CONTROL_DRIVERS, (driver) => driver.make.pipe(Effect.map((instance) => instance.pullRequests)), ); @@ -62,5 +56,4 @@ export const layer = Layer.effect(PullRequestProviderRegistry, make).pipe( ), ), Layer.provide(BuiltInDrivers.layer), - Layer.provide(BitbucketPullRequestApi.layer.pipe(Layer.provide(BitbucketApi.layer))), ); diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 8ea3b5cda198..ff4c4ef6c214 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -52,7 +52,6 @@ import * as ProviderHostLive from "./provider/ProviderHostLive.ts"; import * as AcpRegistrySupport from "@t3tools/provider-acp-registry/server/AcpRegistrySupport"; import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts"; import * as CheckpointStore from "./checkpointing/CheckpointStore.ts"; -import * as BitbucketApi from "./sourceControl/BitbucketApi.ts"; import * as GitHubApi from "./sourceControl/GitHubApi.ts"; import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; import * as SourceControlBuiltInDrivers from "./sourceControl/builtInDrivers.ts"; @@ -297,13 +296,7 @@ const layerPersistence = Layer.empty.pipe(Layer.provideMerge(SqlitePersistence.l const layerVcsDriverRegistry = VcsDriverRegistry.layer.pipe(Layer.provide(VcsProjectConfig.layer)); const layerSourceControlProviderRegistry = SourceControlProviderRegistry.layer.pipe( - Layer.provide( - Layer.mergeAll( - BitbucketApi.layer, - GitHubApi.layerWithDependencies, - SourceControlBuiltInDrivers.layer, - ), - ), + Layer.provide(Layer.mergeAll(GitHubApi.layerWithDependencies, SourceControlBuiltInDrivers.layer)), Layer.provideMerge(GitVcsDriver.layer), Layer.provideMerge(layerVcsDriverRegistry), ); diff --git a/apps/server/src/sourceControl/BitbucketApi.test.ts b/apps/server/src/sourceControl/BitbucketApi.test.ts index 352bfc60d970..85e14a61bc62 100644 --- a/apps/server/src/sourceControl/BitbucketApi.test.ts +++ b/apps/server/src/sourceControl/BitbucketApi.test.ts @@ -11,11 +11,13 @@ import * as TestClock from "effect/testing/TestClock"; import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http"; import { GitCommandError } from "@t3tools/contracts"; -import * as BitbucketApi from "./BitbucketApi.ts"; +import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; import * as ServerSettings from "../serverSettings.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; import type * as VcsDriver from "../vcs/VcsDriver.ts"; +import * as ServerSourceControlHost from "./ServerSourceControlHost.ts"; +import * as VcsProcess from "../vcs/VcsProcess.ts"; const isBitbucketRepositoryLocatorError = Schema.is(BitbucketApi.BitbucketRepositoryLocatorError); @@ -139,6 +141,8 @@ function makeLayer(input: { ); const layer = BitbucketApi.layer.pipe( + Layer.provide(ServerSourceControlHost.layer), + Layer.provide(Layer.mock(VcsProcess.VcsProcess)({})), Layer.provide( Layer.succeed( HttpClient.HttpClient, diff --git a/apps/server/src/sourceControl/ServerSourceControlHost.ts b/apps/server/src/sourceControl/ServerSourceControlHost.ts index f458795ab76e..a21e7d5fe089 100644 --- a/apps/server/src/sourceControl/ServerSourceControlHost.ts +++ b/apps/server/src/sourceControl/ServerSourceControlHost.ts @@ -9,6 +9,8 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as ServerSettings from "../serverSettings.ts"; +import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; +import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; export const layer = Layer.effect( @@ -16,9 +18,23 @@ export const layer = Layer.effect( Effect.gen(function* () { const serverSettings = yield* ServerSettings.ServerSettingsService; const process = yield* VcsProcess.VcsProcess; + const git = yield* GitVcsDriver.GitVcsDriver; + const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry; return SourceControlHost.SourceControlHost.of({ settings: { get: serverSettings.getSettings }, process: { run: process.run }, + git: { + remotes: (cwd) => + vcsRegistry.resolve({ cwd }).pipe(Effect.map((handle) => handle.driver.listRemotes(cwd))), + readConfigValue: git.readConfigValue, + resolvePrimaryRemoteName: git.resolvePrimaryRemoteName, + ensureRemote: git.ensureRemote, + listLocalBranchNames: git.listLocalBranchNames, + fetchRemoteBranch: git.fetchRemoteBranch, + fetchRemoteTrackingBranch: git.fetchRemoteTrackingBranch, + setBranchUpstream: git.setBranchUpstream, + switchRef: git.switchRef, + }, }); }), ); diff --git a/apps/server/src/sourceControl/SourceControlDiscovery.test.ts b/apps/server/src/sourceControl/SourceControlDiscovery.test.ts index c87d731953da..3cda7d31d9fb 100644 --- a/apps/server/src/sourceControl/SourceControlDiscovery.test.ts +++ b/apps/server/src/sourceControl/SourceControlDiscovery.test.ts @@ -18,7 +18,8 @@ import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsCli"; import * as AzureDevOpsPullRequestCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsPullRequestCli"; -import * as BitbucketApi from "./BitbucketApi.ts"; +import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; +import * as BitbucketPullRequestApi from "@t3tools/source-control-bitbucket/server/BitbucketPullRequestApi"; import * as GitHubApi from "./GitHubApi.ts"; import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli"; import * as GitLabPullRequestCli from "@t3tools/source-control-gitlab/server/GitLabPullRequestCli"; @@ -43,6 +44,7 @@ const layerSourceControlProviderRegistryTest = (input: { Layer.mock(AzureDevOpsCli.AzureDevOpsCli)({}), Layer.mock(AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli)({}), Layer.mock(BitbucketApi.BitbucketApi)(input.bitbucket), + Layer.mock(BitbucketPullRequestApi.BitbucketPullRequestApi)({}), ServerSettings.ServerSettingsService.layerTest(), Layer.mock(GitHubApi.GitHubApi)({}), Layer.mock(GitVcsDriver.GitVcsDriver)({}), @@ -53,9 +55,11 @@ const layerSourceControlProviderRegistryTest = (input: { Layer.mock(VcsProcess.VcsProcess)(input.process), ServerSourceControlHost.layer.pipe( Layer.provide( - Layer.merge( + Layer.mergeAll( Layer.mock(VcsProcess.VcsProcess)(input.process), ServerSettings.ServerSettingsService.layerTest(), + Layer.mock(GitVcsDriver.GitVcsDriver)({}), + Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({}), ), ), ), @@ -395,6 +399,7 @@ it.effect( SourceControlHost.SourceControlHost, SourceControlHost.SourceControlHost.of({ settings: host.settings, + git: host.git, process: { run: (input) => { if (input.args[0] !== "fetch") return git.run(input); @@ -529,6 +534,8 @@ it.effect( ServerSourceControlHost.layer.pipe( Layer.provideMerge(VcsProcess.layer), Layer.provideMerge(ServerSettings.ServerSettingsService.layerTest()), + Layer.provideMerge(Layer.mock(GitVcsDriver.GitVcsDriver)({})), + Layer.provideMerge(Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({})), Layer.provideMerge(NodeServices.layer), ), ), diff --git a/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts b/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts index 582848b38a4b..b16ccfa44e73 100644 --- a/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts +++ b/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts @@ -15,7 +15,8 @@ import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsCli"; import * as AzureDevOpsPullRequestCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsPullRequestCli"; -import * as BitbucketApi from "./BitbucketApi.ts"; +import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; +import * as BitbucketPullRequestApi from "@t3tools/source-control-bitbucket/server/BitbucketPullRequestApi"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as GitHubApi from "./GitHubApi.ts"; import * as GitHubCredentials from "./GitHubCredentials.ts"; @@ -101,12 +102,18 @@ function makeRegistry(input: { layerProcess, ServerSourceControlHost.layer.pipe( Layer.provide( - Layer.merge(layerProcess, ServerSettings.ServerSettingsService.layerTest()), + Layer.mergeAll( + layerProcess, + layerRegistry, + ServerSettings.ServerSettingsService.layerTest(), + Layer.mock(GitVcsDriver.GitVcsDriver)({}), + ), ), ), Layer.mock(AzureDevOpsCli.AzureDevOpsCli)({}), Layer.mock(AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli)({}), Layer.mock(BitbucketApi.BitbucketApi)({}), + Layer.mock(BitbucketPullRequestApi.BitbucketPullRequestApi)({}), ServerSettings.ServerSettingsService.layerTest(), Layer.mock(GitHubApi.GitHubApi)({ // No GitHub credential unless a test supplies one, so custom hosts stay unclaimed. diff --git a/apps/server/src/sourceControl/SourceControlProviderRegistry.ts b/apps/server/src/sourceControl/SourceControlProviderRegistry.ts index 9eeae3d935f7..607e1b957b9b 100644 --- a/apps/server/src/sourceControl/SourceControlProviderRegistry.ts +++ b/apps/server/src/sourceControl/SourceControlProviderRegistry.ts @@ -11,7 +11,6 @@ import { import type { SourceControlProviderKind } from "@t3tools/contracts"; import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; -import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; import * as BuiltInDrivers from "./builtInDrivers.ts"; import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; @@ -305,8 +304,6 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit export const make = Effect.gen(function* () { const github = yield* GitHubSourceControlProvider.make; const githubDiscovery = yield* GitHubSourceControlProvider.makeDiscovery; - const bitbucket = yield* BitbucketSourceControlProvider.make; - const bitbucketDiscovery = yield* BitbucketSourceControlProvider.makeDiscovery; const drivers = yield* Effect.forEach(BuiltInDrivers.BUILT_IN_SOURCE_CONTROL_DRIVERS, (driver) => driver.make.pipe( Effect.map((instance): SourceControlProviderRegistration => ({ @@ -324,11 +321,6 @@ export const make = Effect.gen(function* () { provider: github, discovery: githubDiscovery, }, - { - kind: "bitbucket", - provider: bitbucket, - discovery: bitbucketDiscovery, - }, ...drivers, ]; return yield* makeWithProviders( diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts index 8aba02284403..c051ab47f42f 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts @@ -13,8 +13,8 @@ import { GitCommandError, SourceControlProviderError } from "@t3tools/contracts" import * as ServerConfig from "../config.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; -import * as BitbucketApi from "./BitbucketApi.ts"; -import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; +import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; +import * as BitbucketSourceControlProvider from "@t3tools/source-control-bitbucket/server/BitbucketSourceControlProvider"; import type * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; import * as SourceControlRepositoryService from "./SourceControlRepositoryService.ts"; diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.ts index 4a4d1fec6434..1bbca1e2b3d5 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.ts @@ -27,7 +27,7 @@ import { type GitCloneProgressLine, } from "../project/gitCloneProgress.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; -import * as BitbucketApi from "./BitbucketApi.ts"; +import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError); const isSourceControlProviderError = Schema.is(SourceControlProviderError); diff --git a/apps/server/src/sourceControl/builtInDrivers.ts b/apps/server/src/sourceControl/builtInDrivers.ts index 83ee1d35e8f5..9a972e9e5f00 100644 --- a/apps/server/src/sourceControl/builtInDrivers.ts +++ b/apps/server/src/sourceControl/builtInDrivers.ts @@ -10,6 +10,9 @@ import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsCli"; import * as AzureDevOpsPullRequestCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsPullRequestCli"; import * as AzureDevOpsDriver from "@t3tools/source-control-azure-devops/server/driver"; +import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; +import * as BitbucketPullRequestApi from "@t3tools/source-control-bitbucket/server/BitbucketPullRequestApi"; +import * as BitbucketDriver from "@t3tools/source-control-bitbucket/server/driver"; import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; import * as ForgejoDriver from "@t3tools/source-control-forgejo/server/driver"; import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli"; @@ -20,7 +23,12 @@ import * as Layer from "effect/Layer"; import * as ServerSourceControlHost from "./ServerSourceControlHost.ts"; -const drivers = [GitLabDriver.driver, AzureDevOpsDriver.driver, ForgejoDriver.driver]; +const drivers = [ + GitLabDriver.driver, + AzureDevOpsDriver.driver, + BitbucketDriver.driver, + ForgejoDriver.driver, +]; /** Every service a built-in driver's `make` needs; the server's layers must provide them all. */ export type BuiltInSourceControlDriversEnv = @@ -33,6 +41,7 @@ export const BUILT_IN_SOURCE_CONTROL_DRIVERS: ReadonlyArray< /** The services the built-in drivers' packages own, plus the host port they all run against. */ export const layer = Layer.mergeAll( AzureDevOpsPullRequestCli.layer.pipe(Layer.provideMerge(AzureDevOpsCli.layer)), + BitbucketPullRequestApi.layer.pipe(Layer.provideMerge(BitbucketApi.layer)), ForgejoCli.layer, GitLabPullRequestCli.layer.pipe(Layer.provideMerge(GitLabCli.layer)), ).pipe(Layer.provideMerge(ServerSourceControlHost.layer)); diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 19ed2600ba5e..a3b239ffaa62 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -229,7 +229,6 @@ import * as SqlClient from "effect/sql/SqlClient"; import * as PullRequestSyncReactor from "./orchestration-v2/PullRequestSyncReactor.ts"; import * as SourceControlDiscovery from "./sourceControl/SourceControlDiscovery.ts"; import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts"; -import * as BitbucketApi from "./sourceControl/BitbucketApi.ts"; import * as GitHubApi from "./sourceControl/GitHubApi.ts"; import * as SourceControlBuiltInDrivers from "./sourceControl/builtInDrivers.ts"; import * as SourceControlProviderRegistry from "./sourceControl/SourceControlProviderRegistry.ts"; @@ -3202,7 +3201,6 @@ export const layer = Layer.unwrap( SourceControlProviderRegistry.layer.pipe( Layer.provide( Layer.mergeAll( - BitbucketApi.layer, GitHubApi.layerWithDependencies, SourceControlBuiltInDrivers.layer, ), diff --git a/docs/internals/pull-request-file-revisions.md b/docs/internals/pull-request-file-revisions.md index 8fd0dfdb69db..dfc03a436aef 100644 --- a/docs/internals/pull-request-file-revisions.md +++ b/docs/internals/pull-request-file-revisions.md @@ -27,7 +27,7 @@ What "never got to look at" is belongs to the host, and the contract cannot know - [Azure DevOps](../../packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestProvider.ts) reads every version off one iteration listing, so a change too long to follow to its end leaves the paths past that point out. -- [Bitbucket](../../apps/server/src/pullRequest/BitbucketPullRequestApi.ts) reads them off the +- [Bitbucket](../../packages/source-control-bitbucket/src/server/BitbucketPullRequestApi.ts) reads them off the pull request's own patch, the only place it states a file's version, so a patch cut short at the byte ceiling leaves the paths past the cut out. - [GitLab](../../packages/source-control-gitlab/src/server/GitLabPullRequestCli.ts) asks in batches, so a batch diff --git a/package.json b/package.json index 4e0ba35ecc7e..764f45f27c97 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "tc": "vp run -r --concurrency-limit 2 typecheck", "lint": "vp lint --report-unused-disable-directives", "knip": "knip --preprocessor ./scripts/knip-schemas.ts", - "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-acp --workspace packages/provider-acp-registry --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --workspace packages/provider-cursor --workspace packages/provider-grok --workspace packages/source-control-core --workspace packages/source-control-azure-devops --workspace packages/source-control-forgejo --workspace packages/source-control-gitlab --workspace packages/source-control-testing --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", + "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-acp --workspace packages/provider-acp-registry --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --workspace packages/provider-cursor --workspace packages/provider-grok --workspace packages/source-control-core --workspace packages/source-control-azure-devops --workspace packages/source-control-bitbucket --workspace packages/source-control-forgejo --workspace packages/source-control-gitlab --workspace packages/source-control-testing --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", "knip:production": "knip --production --preprocessor ./scripts/knip-schemas.ts", "lint:mobile": "node scripts/mobile-native-static-check.ts", "test": "vp run -r test", diff --git a/packages/source-control-bitbucket/package.json b/packages/source-control-bitbucket/package.json new file mode 100644 index 000000000000..c878093ef54e --- /dev/null +++ b/packages/source-control-bitbucket/package.json @@ -0,0 +1,51 @@ +{ + "name": "@t3tools/source-control-bitbucket", + "private": true, + "type": "module", + "exports": { + "./server/BitbucketApi": { + "types": "./src/server/BitbucketApi.ts", + "import": "./src/server/BitbucketApi.ts" + }, + "./server/BitbucketPullRequestApi": { + "types": "./src/server/BitbucketPullRequestApi.ts", + "import": "./src/server/BitbucketPullRequestApi.ts" + }, + "./server/bitbucketPullRequestJson": { + "types": "./src/server/bitbucketPullRequestJson.ts", + "import": "./src/server/bitbucketPullRequestJson.ts" + }, + "./server/BitbucketPullRequestProvider": { + "types": "./src/server/BitbucketPullRequestProvider.ts", + "import": "./src/server/BitbucketPullRequestProvider.ts" + }, + "./server/bitbucketPullRequests": { + "types": "./src/server/bitbucketPullRequests.ts", + "import": "./src/server/bitbucketPullRequests.ts" + }, + "./server/BitbucketSourceControlProvider": { + "types": "./src/server/BitbucketSourceControlProvider.ts", + "import": "./src/server/BitbucketSourceControlProvider.ts" + }, + "./server/driver": { + "types": "./src/server/driver.ts", + "import": "./src/server/driver.ts" + } + }, + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vp test run --config ../../vite.config.ts --dir ." + }, + "dependencies": { + "@t3tools/contracts": "workspace:*", + "@t3tools/provider-core": "workspace:*", + "@t3tools/shared": "workspace:*", + "@t3tools/source-control-core": "workspace:*", + "effect": "catalog:" + }, + "devDependencies": { + "@effect/vitest": "catalog:", + "@types/node": "catalog:", + "vite-plus": "catalog:" + } +} diff --git a/apps/server/src/sourceControl/BitbucketApi.ts b/packages/source-control-bitbucket/src/server/BitbucketApi.ts similarity index 98% rename from apps/server/src/sourceControl/BitbucketApi.ts rename to packages/source-control-bitbucket/src/server/BitbucketApi.ts index f7fa5cc4568c..3df66527baee 100644 --- a/apps/server/src/sourceControl/BitbucketApi.ts +++ b/packages/source-control-bitbucket/src/server/BitbucketApi.ts @@ -30,9 +30,7 @@ import { } from "./bitbucketPullRequests.ts"; import { collectUint8StreamText } from "@t3tools/provider-core/server/collectStreamText"; import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; -import * as ServerSettings from "../serverSettings.ts"; -import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; -import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; +import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; import { retryAtFromHeader } from "@t3tools/source-control-core/server/SourceControlRateLimit"; const DEFAULT_API_BASE_URL = "https://api.bitbucket.org/2.0"; @@ -390,7 +388,7 @@ export class BitbucketApi extends Context.Service< readonly force?: boolean; }) => Effect.Effect; } ->()("t3/sourceControl/BitbucketApi") {} +>()("@t3tools/source-control-bitbucket/server/BitbucketApi") {} function nonEmpty(value: string | undefined): Option.Option { const trimmed = value?.trim(); @@ -657,16 +655,15 @@ function responseError( /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { const config = yield* BitbucketApiEnvConfig; - const serverSettings = yield* ServerSettings.ServerSettingsService; + const host = yield* SourceControlHost.SourceControlHost; + const git = host.git; const httpClient = yield* HttpClient.HttpClient; const fileSystem = yield* FileSystem.FileSystem; - const git = yield* GitVcsDriver.GitVcsDriver; - const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry; const apiUrl = (path: string) => `${config.baseUrl.replace(/\/+$/u, "")}${path}`; // Read on every request so credentials saved in settings apply without a restart. - const currentCredential = serverSettings.getSettings.pipe( + const currentCredential = host.settings.get.pipe( Effect.map((settings) => resolveCredential(settings.bitbucket, config)), Effect.catch((error) => // No cause: a settings decode error can quote a hand-edited token. @@ -739,7 +736,7 @@ export const make = Effect.gen(function* () { : null; if (fromContext) return fromContext; - const handle = yield* vcsRegistry.resolve({ cwd: input.cwd }).pipe( + const listRemotes = yield* git.remotes(input.cwd).pipe( Effect.mapError( (cause) => new BitbucketRepositoryVcsResolveError({ @@ -748,7 +745,7 @@ export const make = Effect.gen(function* () { }), ), ); - const remotes = yield* handle.driver.listRemotes(input.cwd).pipe( + const remotes = yield* listRemotes.pipe( Effect.mapError( (cause) => new BitbucketRepositoryRemotesListError({ diff --git a/apps/server/src/pullRequest/BitbucketPullRequestApi.test.ts b/packages/source-control-bitbucket/src/server/BitbucketPullRequestApi.test.ts similarity index 99% rename from apps/server/src/pullRequest/BitbucketPullRequestApi.test.ts rename to packages/source-control-bitbucket/src/server/BitbucketPullRequestApi.test.ts index d614dd90a7e7..8d8feaba8150 100644 --- a/apps/server/src/pullRequest/BitbucketPullRequestApi.test.ts +++ b/packages/source-control-bitbucket/src/server/BitbucketPullRequestApi.test.ts @@ -4,7 +4,7 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as TestClock from "effect/testing/TestClock"; -import * as BitbucketApi from "../sourceControl/BitbucketApi.ts"; +import * as BitbucketApi from "./BitbucketApi.ts"; import * as BitbucketPullRequestApi from "./BitbucketPullRequestApi.ts"; const mockedRequest = vi.fn(); diff --git a/apps/server/src/pullRequest/BitbucketPullRequestApi.ts b/packages/source-control-bitbucket/src/server/BitbucketPullRequestApi.ts similarity index 99% rename from apps/server/src/pullRequest/BitbucketPullRequestApi.ts rename to packages/source-control-bitbucket/src/server/BitbucketPullRequestApi.ts index b62b86ffe48f..6dd04a4a0773 100644 --- a/apps/server/src/pullRequest/BitbucketPullRequestApi.ts +++ b/packages/source-control-bitbucket/src/server/BitbucketPullRequestApi.ts @@ -21,7 +21,7 @@ import type { PullRequestReviewerCandidateList, } from "@t3tools/contracts"; -import * as BitbucketApi from "../sourceControl/BitbucketApi.ts"; +import * as BitbucketApi from "./BitbucketApi.ts"; import { parseDiffFileRevisions } from "@t3tools/source-control-core/server/diffFileRevisions"; import { buildReviewThreads, @@ -304,7 +304,7 @@ export class BitbucketPullRequestApi extends Context.Service< readonly resolved: boolean; }) => Effect.Effect; } ->()("t3/pullRequest/BitbucketPullRequestApi") {} +>()("@t3tools/source-control-bitbucket/server/BitbucketPullRequestApi") {} /** `workspace/slug`; Bitbucket has no deeper nesting to address. */ function repositorySegments( diff --git a/apps/server/src/pullRequest/BitbucketPullRequestProvider.test.ts b/packages/source-control-bitbucket/src/server/BitbucketPullRequestProvider.test.ts similarity index 99% rename from apps/server/src/pullRequest/BitbucketPullRequestProvider.test.ts rename to packages/source-control-bitbucket/src/server/BitbucketPullRequestProvider.test.ts index 7c2d58af950e..b5470628be0a 100644 --- a/apps/server/src/pullRequest/BitbucketPullRequestProvider.test.ts +++ b/packages/source-control-bitbucket/src/server/BitbucketPullRequestProvider.test.ts @@ -3,7 +3,7 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Result from "effect/Result"; -import * as BitbucketApi from "../sourceControl/BitbucketApi.ts"; +import * as BitbucketApi from "./BitbucketApi.ts"; import * as BitbucketPullRequestApi from "./BitbucketPullRequestApi.ts"; import { decodePullRequestJson } from "./bitbucketPullRequestJson.ts"; import { diff --git a/apps/server/src/pullRequest/BitbucketPullRequestProvider.ts b/packages/source-control-bitbucket/src/server/BitbucketPullRequestProvider.ts similarity index 100% rename from apps/server/src/pullRequest/BitbucketPullRequestProvider.ts rename to packages/source-control-bitbucket/src/server/BitbucketPullRequestProvider.ts diff --git a/apps/server/src/sourceControl/BitbucketSourceControlProvider.test.ts b/packages/source-control-bitbucket/src/server/BitbucketSourceControlProvider.test.ts similarity index 100% rename from apps/server/src/sourceControl/BitbucketSourceControlProvider.test.ts rename to packages/source-control-bitbucket/src/server/BitbucketSourceControlProvider.test.ts diff --git a/apps/server/src/sourceControl/BitbucketSourceControlProvider.ts b/packages/source-control-bitbucket/src/server/BitbucketSourceControlProvider.ts similarity index 100% rename from apps/server/src/sourceControl/BitbucketSourceControlProvider.ts rename to packages/source-control-bitbucket/src/server/BitbucketSourceControlProvider.ts diff --git a/apps/server/src/pullRequest/bitbucketPullRequestJson.test.ts b/packages/source-control-bitbucket/src/server/bitbucketPullRequestJson.test.ts similarity index 100% rename from apps/server/src/pullRequest/bitbucketPullRequestJson.test.ts rename to packages/source-control-bitbucket/src/server/bitbucketPullRequestJson.test.ts diff --git a/apps/server/src/pullRequest/bitbucketPullRequestJson.ts b/packages/source-control-bitbucket/src/server/bitbucketPullRequestJson.ts similarity index 100% rename from apps/server/src/pullRequest/bitbucketPullRequestJson.ts rename to packages/source-control-bitbucket/src/server/bitbucketPullRequestJson.ts diff --git a/apps/server/src/sourceControl/bitbucketPullRequests.ts b/packages/source-control-bitbucket/src/server/bitbucketPullRequests.ts similarity index 100% rename from apps/server/src/sourceControl/bitbucketPullRequests.ts rename to packages/source-control-bitbucket/src/server/bitbucketPullRequests.ts diff --git a/packages/source-control-bitbucket/src/server/driver.ts b/packages/source-control-bitbucket/src/server/driver.ts new file mode 100644 index 000000000000..06cd225ab4e4 --- /dev/null +++ b/packages/source-control-bitbucket/src/server/driver.ts @@ -0,0 +1,20 @@ +/** + * The Bitbucket Cloud driver: repository operations, discovery, and pull requests through the + * Bitbucket REST API. + * + * @module source-control-bitbucket/server/driver + */ +import { defineSourceControlDriver } from "@t3tools/source-control-core/server/driver"; +import * as Effect from "effect/Effect"; + +import * as BitbucketPullRequestProvider from "./BitbucketPullRequestProvider.ts"; +import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; + +export const driver = defineSourceControlDriver({ + kind: "bitbucket", + make: Effect.all({ + sourceControl: BitbucketSourceControlProvider.make, + discovery: BitbucketSourceControlProvider.makeDiscovery, + pullRequests: BitbucketPullRequestProvider.make, + }), +}); diff --git a/packages/source-control-bitbucket/tsconfig.json b/packages/source-control-bitbucket/tsconfig.json new file mode 100644 index 000000000000..374bac55202d --- /dev/null +++ b/packages/source-control-bitbucket/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["src"] +} diff --git a/packages/source-control-core/src/server/SourceControlHost.ts b/packages/source-control-core/src/server/SourceControlHost.ts index d779f86fe0e2..564b318d85b4 100644 --- a/packages/source-control-core/src/server/SourceControlHost.ts +++ b/packages/source-control-core/src/server/SourceControlHost.ts @@ -7,9 +7,20 @@ * its own API and CLI code. HTTP, the filesystem, and paths come from Effect's platform * services directly. * + * `git` is the subset of the server's git driver that checking out a change request needs; it + * grows only when a provider needs another operation. + * * @module source-control-core/server/SourceControlHost */ -import type { ServerSettings, ServerSettingsError, VcsError } from "@t3tools/contracts"; +import type { + GitCommandError, + ServerSettings, + ServerSettingsError, + VcsError, + VcsListRemotesResult, + VcsSwitchRefInput, + VcsSwitchRefResult, +} from "@t3tools/contracts"; import * as Context from "effect/Context"; import type * as Effect from "effect/Effect"; import type * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; @@ -43,6 +54,12 @@ export interface SourceControlProcessOutput { readonly stderrInvalidUtf8?: boolean; } +export interface SourceControlGitRemoteBranch { + readonly cwd: string; + readonly remoteName: string; + readonly remoteBranch: string; +} + export class SourceControlHost extends Context.Service< SourceControlHost, { @@ -55,5 +72,38 @@ export class SourceControlHost extends Context.Service< input: SourceControlProcessInput, ) => Effect.Effect; }; + readonly git: { + /** + * Lists the remotes of the repository at `cwd`. The outer effect resolves the repository + * and fails when the server cannot drive one there; the inner one lists its remotes. + */ + readonly remotes: ( + cwd: string, + ) => Effect.Effect, VcsError>; + readonly readConfigValue: ( + cwd: string, + key: string, + ) => Effect.Effect; + readonly resolvePrimaryRemoteName: (cwd: string) => Effect.Effect; + /** Adds a remote for `url` unless one exists, and returns the name it is under. */ + readonly ensureRemote: (input: { + readonly cwd: string; + readonly preferredName: string; + readonly url: string; + }) => Effect.Effect; + readonly listLocalBranchNames: (cwd: string) => Effect.Effect; + readonly fetchRemoteBranch: ( + input: SourceControlGitRemoteBranch & { readonly localBranch: string }, + ) => Effect.Effect; + readonly fetchRemoteTrackingBranch: ( + input: SourceControlGitRemoteBranch, + ) => Effect.Effect; + readonly setBranchUpstream: ( + input: SourceControlGitRemoteBranch & { readonly branch: string }, + ) => Effect.Effect; + readonly switchRef: ( + input: VcsSwitchRefInput, + ) => Effect.Effect; + }; } >()("@t3tools/source-control-core/server/SourceControlHost") {} diff --git a/packages/source-control-testing/src/TestSourceControlHost.ts b/packages/source-control-testing/src/TestSourceControlHost.ts index 293339d0867e..eee1a96f13a9 100644 --- a/packages/source-control-testing/src/TestSourceControlHost.ts +++ b/packages/source-control-testing/src/TestSourceControlHost.ts @@ -1,7 +1,7 @@ /** * A `SourceControlHost.SourceControlHost` for provider tests. Settings are fixed unless the test - * supplies its own, and every process run goes to the test's `run`, which fails by default so an - * unexpected CLI call is visible. + * supplies its own. Process runs and git operations go to what the test supplies; anything else + * dies, so an unexpected CLI or git call is visible. * * @module source-control-testing/TestSourceControlHost */ @@ -14,8 +14,26 @@ import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; export interface TestSourceControlHostOptions { readonly settings?: ServerSettings; readonly process?: Partial; + readonly git?: Partial; } +type Git = SourceControlHost.SourceControlHost["Service"]["git"]; + +const unexpectedGit = (operation: string) => () => + Effect.die(`Unexpected git ${operation} in a test that supplied none.`); + +const failingGit: Git = { + remotes: unexpectedGit("remotes"), + readConfigValue: unexpectedGit("readConfigValue"), + resolvePrimaryRemoteName: unexpectedGit("resolvePrimaryRemoteName"), + ensureRemote: unexpectedGit("ensureRemote"), + listLocalBranchNames: unexpectedGit("listLocalBranchNames"), + fetchRemoteBranch: unexpectedGit("fetchRemoteBranch"), + fetchRemoteTrackingBranch: unexpectedGit("fetchRemoteTrackingBranch"), + setBranchUpstream: unexpectedGit("setBranchUpstream"), + switchRef: unexpectedGit("switchRef"), +}; + export const layer = ( options: TestSourceControlHostOptions = {}, ): Layer.Layer => @@ -28,6 +46,7 @@ export const layer = ( options.process?.run ?? ((input) => Effect.die(`Unexpected ${input.command} run in ${input.operation}.`)), }, + git: { ...failingGit, ...options.git }, }), ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13c583be13f0..5ad28290d985 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -651,6 +651,9 @@ importers: '@t3tools/source-control-azure-devops': specifier: workspace:* version: link:../../packages/source-control-azure-devops + '@t3tools/source-control-bitbucket': + specifier: workspace:* + version: link:../../packages/source-control-bitbucket '@t3tools/source-control-core': specifier: workspace:* version: link:../../packages/source-control-core @@ -1556,6 +1559,34 @@ importers: specifier: 1.0.0 version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + packages/source-control-bitbucket: + dependencies: + '@t3tools/contracts': + specifier: workspace:* + version: link:../contracts + '@t3tools/provider-core': + specifier: workspace:* + version: link:../provider-core + '@t3tools/shared': + specifier: workspace:* + version: link:../shared + '@t3tools/source-control-core': + specifier: workspace:* + version: link:../source-control-core + effect: + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) + devDependencies: + '@effect/vitest': + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + '@types/node': + specifier: 24.12.4 + version: 24.12.4 + vite-plus: + specifier: 1.0.0 + version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + packages/source-control-core: dependencies: '@t3tools/contracts': diff --git a/scripts/release-smoke.ts b/scripts/release-smoke.ts index c19a1ee9cac4..96b1df41e1ea 100644 --- a/scripts/release-smoke.ts +++ b/scripts/release-smoke.ts @@ -38,6 +38,7 @@ const workspaceFiles = [ "packages/provider-muse/package.json", "packages/provider-opencode/package.json", "packages/source-control-azure-devops/package.json", + "packages/source-control-bitbucket/package.json", "packages/source-control-core/package.json", "packages/source-control-forgejo/package.json", "packages/source-control-gitlab/package.json", From 507361bc7301982279b9f115bbae8f22268ce9d6 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 14:50:02 -0700 Subject: [PATCH 15/39] refactor(source-control): GitHub lives in @t3tools/source-control-github (#17607) Co-authored-by: Claude Opus 5.5 --- apps/server/package.json | 1 + apps/server/scripts/evaluate-thread-titles.ts | 5 +- apps/server/scripts/measure-pr-preview.ts | 33 ++++--- apps/server/src/assets/AssetAccess.test.ts | 2 +- apps/server/src/assets/GitHubMediaFetch.ts | 2 +- apps/server/src/git/GitManager.test.ts | 2 +- .../PullRequestProviderRateLimit.test.ts | 4 +- .../PullRequestProviderRegistry.ts | 19 +---- .../src/pullRequest/PullRequestService.ts | 2 +- apps/server/src/server.ts | 7 +- .../sourceControl/ServerSourceControlHost.ts | 2 + .../SourceControlDiscovery.test.ts | 4 +- .../SourceControlProviderRegistry.test.ts | 6 +- .../SourceControlProviderRegistry.ts | 17 +--- .../src/sourceControl/builtInDrivers.ts | 8 ++ .../githubStackRebase.test.ts | 17 +++- apps/server/src/ws.ts | 8 +- package.json | 2 +- .../src/server/SourceControlHost.ts | 15 ++++ packages/source-control-github/package.json | 85 +++++++++++++++++++ .../src/server}/GitHubApi.test.ts | 14 +-- .../src/server}/GitHubApi.ts | 10 ++- .../src/server}/GitHubCredentials.test.ts | 66 ++++++++------ .../src/server}/GitHubCredentials.ts | 13 ++- .../src/server}/GitHubPullRequestApi.test.ts | 15 ++-- .../src/server}/GitHubPullRequestApi.ts | 12 +-- .../server}/GitHubPullRequestProvider.test.ts | 2 +- .../src/server}/GitHubPullRequestProvider.ts | 2 +- .../src/server/GitHubQuota.test.ts | 2 +- .../src/server/GitHubQuota.ts | 2 +- .../GitHubSourceControlProvider.test.ts | 35 ++++---- .../server}/GitHubSourceControlProvider.ts | 15 ++-- .../src/server/driver.ts | 20 +++++ .../src/server}/gitHubAuthStatus.ts | 0 .../server}/gitHubConditionalChecks.test.ts | 2 +- .../src/server}/gitHubConditionalChecks.ts | 6 +- .../src/server}/gitHubPullRequestJson.test.ts | 0 .../src/server}/gitHubPullRequestJson.ts | 2 +- .../src/server}/gitHubPullRequests.ts | 0 .../gitHubRepositoryResolution.test.ts | 0 .../src/server}/gitHubRepositoryResolution.ts | 0 .../src/server}/githubGraphQl.test.ts | 0 .../src/server}/githubGraphQl.ts | 0 .../src/server}/githubStackActions.test.ts | 34 ++++---- .../src/server}/githubStackActions.ts | 4 +- .../src/server}/githubStackRebase.ts | 6 +- packages/source-control-github/tsconfig.json | 7 ++ packages/source-control-testing/package.json | 1 + .../src/TestSourceControlHost.ts | 54 ++++++++---- pnpm-lock.yaml | 40 +++++++++ scripts/release-smoke.ts | 1 + 51 files changed, 411 insertions(+), 195 deletions(-) rename apps/server/src/{pullRequest => sourceControl}/githubStackRebase.test.ts (88%) create mode 100644 packages/source-control-github/package.json rename {apps/server/src/sourceControl => packages/source-control-github/src/server}/GitHubApi.test.ts (97%) rename {apps/server/src/sourceControl => packages/source-control-github/src/server}/GitHubApi.ts (98%) rename {apps/server/src/sourceControl => packages/source-control-github/src/server}/GitHubCredentials.test.ts (76%) rename {apps/server/src/sourceControl => packages/source-control-github/src/server}/GitHubCredentials.ts (96%) rename {apps/server/src/pullRequest => packages/source-control-github/src/server}/GitHubPullRequestApi.test.ts (99%) rename {apps/server/src/pullRequest => packages/source-control-github/src/server}/GitHubPullRequestApi.ts (99%) rename {apps/server/src/pullRequest => packages/source-control-github/src/server}/GitHubPullRequestProvider.test.ts (99%) rename {apps/server/src/pullRequest => packages/source-control-github/src/server}/GitHubPullRequestProvider.ts (99%) rename apps/server/src/sourceControl/githubQuota.test.ts => packages/source-control-github/src/server/GitHubQuota.test.ts (98%) rename apps/server/src/sourceControl/githubQuota.ts => packages/source-control-github/src/server/GitHubQuota.ts (98%) rename {apps/server/src/sourceControl => packages/source-control-github/src/server}/GitHubSourceControlProvider.test.ts (98%) rename {apps/server/src/sourceControl => packages/source-control-github/src/server}/GitHubSourceControlProvider.ts (98%) create mode 100644 packages/source-control-github/src/server/driver.ts rename {apps/server/src/sourceControl => packages/source-control-github/src/server}/gitHubAuthStatus.ts (100%) rename {apps/server/src/pullRequest => packages/source-control-github/src/server}/gitHubConditionalChecks.test.ts (99%) rename {apps/server/src/pullRequest => packages/source-control-github/src/server}/gitHubConditionalChecks.ts (97%) rename {apps/server/src/pullRequest => packages/source-control-github/src/server}/gitHubPullRequestJson.test.ts (100%) rename {apps/server/src/pullRequest => packages/source-control-github/src/server}/gitHubPullRequestJson.ts (99%) rename {apps/server/src/sourceControl => packages/source-control-github/src/server}/gitHubPullRequests.ts (100%) rename {apps/server/src/sourceControl => packages/source-control-github/src/server}/gitHubRepositoryResolution.test.ts (100%) rename {apps/server/src/sourceControl => packages/source-control-github/src/server}/gitHubRepositoryResolution.ts (100%) rename {apps/server/src/sourceControl => packages/source-control-github/src/server}/githubGraphQl.test.ts (100%) rename {apps/server/src/sourceControl => packages/source-control-github/src/server}/githubGraphQl.ts (100%) rename {apps/server/src/pullRequest => packages/source-control-github/src/server}/githubStackActions.test.ts (95%) rename {apps/server/src/pullRequest => packages/source-control-github/src/server}/githubStackActions.ts (98%) rename {apps/server/src/pullRequest => packages/source-control-github/src/server}/githubStackRebase.ts (96%) create mode 100644 packages/source-control-github/tsconfig.json diff --git a/apps/server/package.json b/apps/server/package.json index ab9ad49d4e62..3dd02fcdcb5e 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -49,6 +49,7 @@ "@t3tools/source-control-bitbucket": "workspace:*", "@t3tools/source-control-core": "workspace:*", "@t3tools/source-control-forgejo": "workspace:*", + "@t3tools/source-control-github": "workspace:*", "@t3tools/source-control-gitlab": "workspace:*", "effect": "catalog:", "jose": "catalog:", diff --git a/apps/server/scripts/evaluate-thread-titles.ts b/apps/server/scripts/evaluate-thread-titles.ts index 598f6cc2f930..cb1553b5cdcf 100644 --- a/apps/server/scripts/evaluate-thread-titles.ts +++ b/apps/server/scripts/evaluate-thread-titles.ts @@ -23,7 +23,6 @@ import { } from "@t3tools/provider-core/server/threadTitleContext"; import * as ThreadTitleLinks from "../src/textGeneration/ThreadTitleLinks.ts"; import * as SourceControlProviderRegistry from "../src/sourceControl/SourceControlProviderRegistry.ts"; -import * as GitHubApi from "../src/sourceControl/GitHubApi.ts"; import * as SourceControlBuiltInDrivers from "../src/sourceControl/builtInDrivers.ts"; import * as ServerSettings from "../src/serverSettings.ts"; import * as VcsProcess from "../src/vcs/VcsProcess.ts"; @@ -147,9 +146,7 @@ await Effect.runPromise( Layer.mergeAll( ProcessRunner.layer, SourceControlProviderRegistry.layer.pipe( - Layer.provide( - Layer.mergeAll(GitHubApi.layerWithDependencies, SourceControlBuiltInDrivers.layer), - ), + Layer.provide(SourceControlBuiltInDrivers.layer), // Default settings: no saved Bitbucket token, gh's own GitHub account choice. Layer.provide(ServerSettings.layerTest()), Layer.provide(VcsDriverRegistry.layer.pipe(Layer.provide(VcsProjectConfig.layer))), diff --git a/apps/server/scripts/measure-pr-preview.ts b/apps/server/scripts/measure-pr-preview.ts index 0f33ec9a6139..8fca30c91486 100644 --- a/apps/server/scripts/measure-pr-preview.ts +++ b/apps/server/scripts/measure-pr-preview.ts @@ -10,14 +10,19 @@ import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import { FetchHttpClient } from "effect/http"; -import * as GitHubPullRequestApi from "../src/pullRequest/GitHubPullRequestApi.ts"; -import * as GitHubPullRequestProvider from "../src/pullRequest/GitHubPullRequestProvider.ts"; -import * as GitHubApi from "../src/sourceControl/GitHubApi.ts"; -import * as GitHubCredentials from "../src/sourceControl/GitHubCredentials.ts"; +import * as GitHubPullRequestApi from "@t3tools/source-control-github/server/GitHubPullRequestApi"; +import * as GitHubPullRequestProvider from "@t3tools/source-control-github/server/GitHubPullRequestProvider"; +import * as GitHubApi from "@t3tools/source-control-github/server/GitHubApi"; +import * as GitHubCredentials from "@t3tools/source-control-github/server/GitHubCredentials"; +import * as ServerConfig from "../src/config.ts"; import * as ServerSettings from "../src/serverSettings.ts"; -import * as GitHubQuota from "../src/sourceControl/githubQuota.ts"; +import * as GitHubQuota from "@t3tools/source-control-github/server/GitHubQuota"; import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; +import * as ServerSourceControlHost from "../src/sourceControl/ServerSourceControlHost.ts"; +import * as GitVcsDriver from "../src/vcs/GitVcsDriver.ts"; +import * as VcsDriverRegistry from "../src/vcs/VcsDriverRegistry.ts"; import * as VcsProcess from "../src/vcs/VcsProcess.ts"; +import * as VcsProjectConfig from "../src/vcs/VcsProjectConfig.ts"; const [repository, ...numbers] = process.argv.slice(2); if (!repository || numbers.length === 0 || numbers.some((number) => !/^\d+$/.test(number))) { @@ -41,6 +46,17 @@ function withRateLimit(query: string): string { : `${query.slice(0, end)}\n rateLimit { cost }\n${query.slice(end)}`; } +// The server's host port over default settings: gh's own account choice, no saved token. +const sourceControlHost = ServerSourceControlHost.layer.pipe( + Layer.provide(VcsDriverRegistry.layer.pipe(Layer.provide(VcsProjectConfig.layer))), + Layer.provide(GitVcsDriver.layer), + Layer.provide(ServerSettings.layerTest()), + // A scratch state directory, so nothing here touches a real T3 home. + Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-measure-pr-preview-" })), + Layer.provideMerge(VcsProcess.layer), + Layer.provideMerge(NodeServices.layer), +); + const measuredApi = Layer.effect( GitHubApi.GitHubApi, Effect.gen(function* () { @@ -73,18 +89,15 @@ const measuredApi = Layer.effect( }); }), ).pipe( - Layer.provide(GitHubCredentials.layer.pipe(Layer.provide(ServerSettings.layerTest()))), + Layer.provide(GitHubCredentials.layer), Layer.provide(GitHubQuota.layer), Layer.provide(SourceControlRateLimit.layer), Layer.provide(FetchHttpClient.layer), - Layer.provide(VcsProcess.layer), - Layer.provide(NodeServices.layer), ); const services = GitHubPullRequestApi.layer.pipe( Layer.provideMerge(measuredApi), - Layer.provideMerge(VcsProcess.layer), - Layer.provideMerge(NodeServices.layer), + Layer.provideMerge(sourceControlHost), ); const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); diff --git a/apps/server/src/assets/AssetAccess.test.ts b/apps/server/src/assets/AssetAccess.test.ts index 12ec00256973..61197efb6a50 100644 --- a/apps/server/src/assets/AssetAccess.test.ts +++ b/apps/server/src/assets/AssetAccess.test.ts @@ -35,7 +35,7 @@ import { ASSET_ROUTE_PREFIX, issueAssetUrl, resolveAsset } from "./AssetAccess.t import * as NativeAppIconResolver from "./NativeAppIconResolver.ts"; import { openMediaFile } from "./MediaFile.ts"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; -import * as GitHubCredentials from "../sourceControl/GitHubCredentials.ts"; +import * as GitHubCredentials from "@t3tools/source-control-github/server/GitHubCredentials"; import { githubMediaResponse } from "./GitHubMediaFetch.ts"; vi.mock("node:fs/promises", async (importOriginal) => { diff --git a/apps/server/src/assets/GitHubMediaFetch.ts b/apps/server/src/assets/GitHubMediaFetch.ts index 7b26c3e69acd..d05308f2afed 100644 --- a/apps/server/src/assets/GitHubMediaFetch.ts +++ b/apps/server/src/assets/GitHubMediaFetch.ts @@ -12,7 +12,7 @@ import { type HttpClientResponse, } from "effect/http"; -import * as GitHubCredentials from "../sourceControl/GitHubCredentials.ts"; +import * as GitHubCredentials from "@t3tools/source-control-github/server/GitHubCredentials"; /** * Exactly the hosts the credential is for. Everything a redirect leads to — the presigned diff --git a/apps/server/src/git/GitManager.test.ts b/apps/server/src/git/GitManager.test.ts index 92b7fcd607fc..24dd7e1d54ea 100644 --- a/apps/server/src/git/GitManager.test.ts +++ b/apps/server/src/git/GitManager.test.ts @@ -43,7 +43,7 @@ import { ThreadId, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; -import { decodeGitHubPullRequestListJson } from "../sourceControl/gitHubPullRequests.ts"; +import { decodeGitHubPullRequestListJson } from "@t3tools/source-control-github/server/gitHubPullRequests"; import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli"; import type * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; import * as TestSourceControlHost from "@t3tools/source-control-testing/TestSourceControlHost"; diff --git a/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts b/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts index 6d35adc07246..8271c2f22dc0 100644 --- a/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts +++ b/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts @@ -2,12 +2,12 @@ import { assert, it } from "@effect/vitest"; import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsCli"; import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; -import * as GitHubApi from "../sourceControl/GitHubApi.ts"; +import * as GitHubApi from "@t3tools/source-control-github/server/GitHubApi"; import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli"; import { azureDevOpsProviderFailure } from "@t3tools/source-control-azure-devops/server/AzureDevOpsPullRequestProvider"; import { bitbucketProviderFailure } from "@t3tools/source-control-bitbucket/server/BitbucketPullRequestProvider"; -import { gitHubProviderFailure } from "./GitHubPullRequestProvider.ts"; +import { gitHubProviderFailure } from "@t3tools/source-control-github/server/GitHubPullRequestProvider"; import { gitLabProviderFailure } from "@t3tools/source-control-gitlab/server/GitLabPullRequestProvider"; const cause = new Error("redacted provider failure"); diff --git a/apps/server/src/pullRequest/PullRequestProviderRegistry.ts b/apps/server/src/pullRequest/PullRequestProviderRegistry.ts index 5702957a0ff4..66607a4c8abd 100644 --- a/apps/server/src/pullRequest/PullRequestProviderRegistry.ts +++ b/apps/server/src/pullRequest/PullRequestProviderRegistry.ts @@ -3,9 +3,6 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import type { SourceControlProviderKind } from "@t3tools/contracts"; -import * as GitHubApi from "../sourceControl/GitHubApi.ts"; -import * as GitHubPullRequestApi from "./GitHubPullRequestApi.ts"; -import * as GitHubPullRequestProvider from "./GitHubPullRequestProvider.ts"; import * as BuiltInDrivers from "../sourceControl/builtInDrivers.ts"; import type { PullRequestProviderApi } from "@t3tools/source-control-core/server/PullRequestProvider"; @@ -36,24 +33,14 @@ export function fromProviders( * @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { - const providers = yield* Effect.all([GitHubPullRequestProvider.make]); const drivers = yield* Effect.forEach(BuiltInDrivers.BUILT_IN_SOURCE_CONTROL_DRIVERS, (driver) => driver.make.pipe(Effect.map((instance) => instance.pullRequests)), ); - return fromProviders([ - ...providers, - ...drivers.filter((provider): provider is PullRequestProviderApi => provider !== null), - ]); + return fromProviders( + drivers.filter((provider): provider is PullRequestProviderApi => provider !== null), + ); }); export const layer = Layer.effect(PullRequestProviderRegistry, make).pipe( - Layer.provide( - GitHubPullRequestApi.layer.pipe( - Layer.provide( - // Built here too, so a registry built alone still has one budget and pause per host. - GitHubApi.layerWithDependencies, - ), - ), - ), Layer.provide(BuiltInDrivers.layer), ); diff --git a/apps/server/src/pullRequest/PullRequestService.ts b/apps/server/src/pullRequest/PullRequestService.ts index fb14ef896cd5..0295673350b7 100644 --- a/apps/server/src/pullRequest/PullRequestService.ts +++ b/apps/server/src/pullRequest/PullRequestService.ts @@ -77,7 +77,7 @@ import { import { resolveProjectSettings } from "@t3tools/shared/projectSettings"; import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; -import { AllowGitHubReserve } from "../sourceControl/GitHubApi.ts"; +import { AllowGitHubReserve } from "@t3tools/source-control-github/server/GitHubApi"; import * as ProjectService from "../project/ProjectService.ts"; import * as ServerSettings from "../serverSettings.ts"; import * as PullRequestFilesViewed from "../persistence/PullRequestFilesViewed.ts"; diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index ff4c4ef6c214..a5e23ed65b56 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -52,7 +52,6 @@ import * as ProviderHostLive from "./provider/ProviderHostLive.ts"; import * as AcpRegistrySupport from "@t3tools/provider-acp-registry/server/AcpRegistrySupport"; import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts"; import * as CheckpointStore from "./checkpointing/CheckpointStore.ts"; -import * as GitHubApi from "./sourceControl/GitHubApi.ts"; import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; import * as SourceControlBuiltInDrivers from "./sourceControl/builtInDrivers.ts"; import * as TextGeneration from "./textGeneration/TextGeneration.ts"; @@ -296,7 +295,7 @@ const layerPersistence = Layer.empty.pipe(Layer.provideMerge(SqlitePersistence.l const layerVcsDriverRegistry = VcsDriverRegistry.layer.pipe(Layer.provide(VcsProjectConfig.layer)); const layerSourceControlProviderRegistry = SourceControlProviderRegistry.layer.pipe( - Layer.provide(Layer.mergeAll(GitHubApi.layerWithDependencies, SourceControlBuiltInDrivers.layer)), + Layer.provideMerge(SourceControlBuiltInDrivers.layer), Layer.provideMerge(GitVcsDriver.layer), Layer.provideMerge(layerVcsDriverRegistry), ); @@ -585,9 +584,9 @@ const layerRuntimeCoreDependenciesBase = Layer.mergeAll( Layer.provideMerge(RuntimeLayer.layerEventInfrastructure), Layer.provideMerge(Layer.merge(ProjectStore.layer, ThreadSearch.layer)), Layer.provideMerge(layerServerSettings), - // The asset route uses the registry's GitHub credential for private PR media. + // The asset route uses the registry's GitHub credential for private PR media, which the + // built-in drivers' layer provides alongside the registry. Layer.provideMerge(layerSourceControlProviderRegistry), - Layer.provideMerge(GitHubApi.layerWithDependencies), Layer.provideMerge(layerGit), Layer.provideMerge(layerVcs), Layer.provideMerge(Layer.mergeAll(layerTerminal, layerPreview, layerDevice)), diff --git a/apps/server/src/sourceControl/ServerSourceControlHost.ts b/apps/server/src/sourceControl/ServerSourceControlHost.ts index a21e7d5fe089..c5c55fd85ab8 100644 --- a/apps/server/src/sourceControl/ServerSourceControlHost.ts +++ b/apps/server/src/sourceControl/ServerSourceControlHost.ts @@ -24,6 +24,8 @@ export const layer = Layer.effect( settings: { get: serverSettings.getSettings }, process: { run: process.run }, git: { + execute: git.execute, + resolveCommit: git.resolveCommit, remotes: (cwd) => vcsRegistry.resolve({ cwd }).pipe(Effect.map((handle) => handle.driver.listRemotes(cwd))), readConfigValue: git.readConfigValue, diff --git a/apps/server/src/sourceControl/SourceControlDiscovery.test.ts b/apps/server/src/sourceControl/SourceControlDiscovery.test.ts index 3cda7d31d9fb..b4503fb7535f 100644 --- a/apps/server/src/sourceControl/SourceControlDiscovery.test.ts +++ b/apps/server/src/sourceControl/SourceControlDiscovery.test.ts @@ -20,7 +20,8 @@ import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/Azu import * as AzureDevOpsPullRequestCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsPullRequestCli"; import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; import * as BitbucketPullRequestApi from "@t3tools/source-control-bitbucket/server/BitbucketPullRequestApi"; -import * as GitHubApi from "./GitHubApi.ts"; +import * as GitHubApi from "@t3tools/source-control-github/server/GitHubApi"; +import * as GitHubPullRequestApi from "@t3tools/source-control-github/server/GitHubPullRequestApi"; import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli"; import * as GitLabPullRequestCli from "@t3tools/source-control-gitlab/server/GitLabPullRequestCli"; import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; @@ -46,6 +47,7 @@ const layerSourceControlProviderRegistryTest = (input: { Layer.mock(BitbucketApi.BitbucketApi)(input.bitbucket), Layer.mock(BitbucketPullRequestApi.BitbucketPullRequestApi)({}), ServerSettings.ServerSettingsService.layerTest(), + Layer.mock(GitHubPullRequestApi.GitHubPullRequestApi)({}), Layer.mock(GitHubApi.GitHubApi)({}), Layer.mock(GitVcsDriver.GitVcsDriver)({}), Layer.mock(GitLabCli.GitLabCli)({}), diff --git a/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts b/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts index b16ccfa44e73..bfed745ca295 100644 --- a/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts +++ b/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts @@ -18,8 +18,9 @@ import * as AzureDevOpsPullRequestCli from "@t3tools/source-control-azure-devops import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; import * as BitbucketPullRequestApi from "@t3tools/source-control-bitbucket/server/BitbucketPullRequestApi"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; -import * as GitHubApi from "./GitHubApi.ts"; -import * as GitHubCredentials from "./GitHubCredentials.ts"; +import * as GitHubApi from "@t3tools/source-control-github/server/GitHubApi"; +import * as GitHubPullRequestApi from "@t3tools/source-control-github/server/GitHubPullRequestApi"; +import * as GitHubCredentials from "@t3tools/source-control-github/server/GitHubCredentials"; import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli"; import * as GitLabPullRequestCli from "@t3tools/source-control-gitlab/server/GitLabPullRequestCli"; import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; @@ -115,6 +116,7 @@ function makeRegistry(input: { Layer.mock(BitbucketApi.BitbucketApi)({}), Layer.mock(BitbucketPullRequestApi.BitbucketPullRequestApi)({}), ServerSettings.ServerSettingsService.layerTest(), + Layer.mock(GitHubPullRequestApi.GitHubPullRequestApi)({}), Layer.mock(GitHubApi.GitHubApi)({ // No GitHub credential unless a test supplies one, so custom hosts stay unclaimed. credential: (host) => Effect.fail(new GitHubCredentials.GitHubNotSignedInError({ host })), diff --git a/apps/server/src/sourceControl/SourceControlProviderRegistry.ts b/apps/server/src/sourceControl/SourceControlProviderRegistry.ts index 607e1b957b9b..153e8c21b113 100644 --- a/apps/server/src/sourceControl/SourceControlProviderRegistry.ts +++ b/apps/server/src/sourceControl/SourceControlProviderRegistry.ts @@ -11,7 +11,6 @@ import { import type { SourceControlProviderKind } from "@t3tools/contracts"; import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; -import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; import * as BuiltInDrivers from "./builtInDrivers.ts"; import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; import { @@ -302,8 +301,6 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit ); export const make = Effect.gen(function* () { - const github = yield* GitHubSourceControlProvider.make; - const githubDiscovery = yield* GitHubSourceControlProvider.makeDiscovery; const drivers = yield* Effect.forEach(BuiltInDrivers.BUILT_IN_SOURCE_CONTROL_DRIVERS, (driver) => driver.make.pipe( Effect.map((instance): SourceControlProviderRegistration => ({ @@ -313,19 +310,7 @@ export const make = Effect.gen(function* () { })), ), ); - // Discovery lists hosts in this order, so it is kept while hosts move into packages. - const order = ["github", "gitlab", "azure-devops", "bitbucket", "forgejo"]; - const registrations: ReadonlyArray = [ - { - kind: "github", - provider: github, - discovery: githubDiscovery, - }, - ...drivers, - ]; - return yield* makeWithProviders( - registrations.toSorted((left, right) => order.indexOf(left.kind) - order.indexOf(right.kind)), - ); + return yield* makeWithProviders(drivers); }); export const layer = Layer.effect(SourceControlProviderRegistry, make); diff --git a/apps/server/src/sourceControl/builtInDrivers.ts b/apps/server/src/sourceControl/builtInDrivers.ts index 9a972e9e5f00..2e830dec803e 100644 --- a/apps/server/src/sourceControl/builtInDrivers.ts +++ b/apps/server/src/sourceControl/builtInDrivers.ts @@ -15,6 +15,9 @@ import * as BitbucketPullRequestApi from "@t3tools/source-control-bitbucket/serv import * as BitbucketDriver from "@t3tools/source-control-bitbucket/server/driver"; import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; import * as ForgejoDriver from "@t3tools/source-control-forgejo/server/driver"; +import * as GitHubApi from "@t3tools/source-control-github/server/GitHubApi"; +import * as GitHubPullRequestApi from "@t3tools/source-control-github/server/GitHubPullRequestApi"; +import * as GitHubDriver from "@t3tools/source-control-github/server/driver"; import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli"; import * as GitLabPullRequestCli from "@t3tools/source-control-gitlab/server/GitLabPullRequestCli"; import * as GitLabDriver from "@t3tools/source-control-gitlab/server/driver"; @@ -24,6 +27,7 @@ import * as Layer from "effect/Layer"; import * as ServerSourceControlHost from "./ServerSourceControlHost.ts"; const drivers = [ + GitHubDriver.driver, GitLabDriver.driver, AzureDevOpsDriver.driver, BitbucketDriver.driver, @@ -34,12 +38,16 @@ const drivers = [ export type BuiltInSourceControlDriversEnv = (typeof drivers)[number] extends SourceControlDriver ? R : never; +/** Ordered as the hosts appear in discovery. */ export const BUILT_IN_SOURCE_CONTROL_DRIVERS: ReadonlyArray< SourceControlDriver > = drivers; /** The services the built-in drivers' packages own, plus the host port they all run against. */ export const layer = Layer.mergeAll( + // `GitHubApi.layerWithDependencies` carries the quota reserve and rate-limit pause every GitHub + // reader shares, so the server builds it once, here. + GitHubPullRequestApi.layer.pipe(Layer.provideMerge(GitHubApi.layerWithDependencies)), AzureDevOpsPullRequestCli.layer.pipe(Layer.provideMerge(AzureDevOpsCli.layer)), BitbucketPullRequestApi.layer.pipe(Layer.provideMerge(BitbucketApi.layer)), ForgejoCli.layer, diff --git a/apps/server/src/pullRequest/githubStackRebase.test.ts b/apps/server/src/sourceControl/githubStackRebase.test.ts similarity index 88% rename from apps/server/src/pullRequest/githubStackRebase.test.ts rename to apps/server/src/sourceControl/githubStackRebase.test.ts index 26b801d13bd8..613c0016c65f 100644 --- a/apps/server/src/pullRequest/githubStackRebase.test.ts +++ b/apps/server/src/sourceControl/githubStackRebase.test.ts @@ -5,15 +5,26 @@ import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Redacted from "effect/Redacted"; -import * as GitHubApi from "../sourceControl/GitHubApi.ts"; +import * as GitHubApi from "@t3tools/source-control-github/server/GitHubApi"; +import { cascadeRebaseStack } from "@t3tools/source-control-github/server/githubStackRebase"; + +import * as ServerSettings from "../serverSettings.ts"; +import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; +import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; -import { cascadeRebaseStack } from "./githubStackRebase.ts"; +import * as ServerSourceControlHost from "./ServerSourceControlHost.ts"; const layer = Layer.mergeAll( Layer.mock(GitHubApi.GitHubApi)({ credential: () => Effect.succeed({ token: Redacted.make("token"), fingerprint: "fp" }), }), - VcsProcess.layer, + // The cascade runs real git through the server's host; the git driver itself is never asked. + ServerSourceControlHost.layer.pipe( + Layer.provide(ServerSettings.layerTest()), + Layer.provide(Layer.mock(GitVcsDriver.GitVcsDriver)({})), + Layer.provide(Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({})), + Layer.provideMerge(VcsProcess.layer), + ), ).pipe(Layer.provideMerge(NodeServices.layer)); /** diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index a3b239ffaa62..8e7a1210a507 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -229,7 +229,6 @@ import * as SqlClient from "effect/sql/SqlClient"; import * as PullRequestSyncReactor from "./orchestration-v2/PullRequestSyncReactor.ts"; import * as SourceControlDiscovery from "./sourceControl/SourceControlDiscovery.ts"; import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts"; -import * as GitHubApi from "./sourceControl/GitHubApi.ts"; import * as SourceControlBuiltInDrivers from "./sourceControl/builtInDrivers.ts"; import * as SourceControlProviderRegistry from "./sourceControl/SourceControlProviderRegistry.ts"; import * as GitVcsDriver from "./vcs/GitVcsDriver.ts"; @@ -3199,12 +3198,7 @@ export const layer = Layer.unwrap( SourceControlDiscovery.layer.pipe( Layer.provide( SourceControlProviderRegistry.layer.pipe( - Layer.provide( - Layer.mergeAll( - GitHubApi.layerWithDependencies, - SourceControlBuiltInDrivers.layer, - ), - ), + Layer.provide(SourceControlBuiltInDrivers.layer), Layer.provideMerge(GitVcsDriver.layer), Layer.provide( VcsDriverRegistry.layer.pipe(Layer.provide(VcsProjectConfig.layer)), diff --git a/package.json b/package.json index 764f45f27c97..ba161940d2f7 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "tc": "vp run -r --concurrency-limit 2 typecheck", "lint": "vp lint --report-unused-disable-directives", "knip": "knip --preprocessor ./scripts/knip-schemas.ts", - "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-acp --workspace packages/provider-acp-registry --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --workspace packages/provider-cursor --workspace packages/provider-grok --workspace packages/source-control-core --workspace packages/source-control-azure-devops --workspace packages/source-control-bitbucket --workspace packages/source-control-forgejo --workspace packages/source-control-gitlab --workspace packages/source-control-testing --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", + "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-acp --workspace packages/provider-acp-registry --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --workspace packages/provider-cursor --workspace packages/provider-grok --workspace packages/source-control-core --workspace packages/source-control-azure-devops --workspace packages/source-control-bitbucket --workspace packages/source-control-forgejo --workspace packages/source-control-github --workspace packages/source-control-gitlab --workspace packages/source-control-testing --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", "knip:production": "knip --production --preprocessor ./scripts/knip-schemas.ts", "lint:mobile": "node scripts/mobile-native-static-check.ts", "test": "vp run -r test", diff --git a/packages/source-control-core/src/server/SourceControlHost.ts b/packages/source-control-core/src/server/SourceControlHost.ts index 564b318d85b4..a46db0fccf27 100644 --- a/packages/source-control-core/src/server/SourceControlHost.ts +++ b/packages/source-control-core/src/server/SourceControlHost.ts @@ -54,6 +54,11 @@ export interface SourceControlProcessOutput { readonly stderrInvalidUtf8?: boolean; } +export type SourceControlGitOutput = Omit< + SourceControlProcessOutput, + "stdoutInvalidUtf8" | "stderrInvalidUtf8" +>; + export interface SourceControlGitRemoteBranch { readonly cwd: string; readonly remoteName: string; @@ -73,6 +78,16 @@ export class SourceControlHost extends Context.Service< ) => Effect.Effect; }; readonly git: { + /** Runs `git -C cwd …args` through the server's git driver, with its limits and metrics. */ + readonly execute: (input: { + readonly operation: string; + readonly cwd: string; + readonly args: ReadonlyArray; + }) => Effect.Effect; + readonly resolveCommit: (input: { + readonly cwd: string; + readonly revision: string; + }) => Effect.Effect<{ readonly commitSha: string }, GitCommandError>; /** * Lists the remotes of the repository at `cwd`. The outer effect resolves the repository * and fails when the server cannot drive one there; the inner one lists its remotes. diff --git a/packages/source-control-github/package.json b/packages/source-control-github/package.json new file mode 100644 index 000000000000..739d6e8b5f3f --- /dev/null +++ b/packages/source-control-github/package.json @@ -0,0 +1,85 @@ +{ + "name": "@t3tools/source-control-github", + "private": true, + "type": "module", + "exports": { + "./server/driver": { + "types": "./src/server/driver.ts", + "import": "./src/server/driver.ts" + }, + "./server/GitHubApi": { + "types": "./src/server/GitHubApi.ts", + "import": "./src/server/GitHubApi.ts" + }, + "./server/gitHubAuthStatus": { + "types": "./src/server/gitHubAuthStatus.ts", + "import": "./src/server/gitHubAuthStatus.ts" + }, + "./server/gitHubConditionalChecks": { + "types": "./src/server/gitHubConditionalChecks.ts", + "import": "./src/server/gitHubConditionalChecks.ts" + }, + "./server/GitHubCredentials": { + "types": "./src/server/GitHubCredentials.ts", + "import": "./src/server/GitHubCredentials.ts" + }, + "./server/githubGraphQl": { + "types": "./src/server/githubGraphQl.ts", + "import": "./src/server/githubGraphQl.ts" + }, + "./server/GitHubPullRequestApi": { + "types": "./src/server/GitHubPullRequestApi.ts", + "import": "./src/server/GitHubPullRequestApi.ts" + }, + "./server/gitHubPullRequestJson": { + "types": "./src/server/gitHubPullRequestJson.ts", + "import": "./src/server/gitHubPullRequestJson.ts" + }, + "./server/GitHubPullRequestProvider": { + "types": "./src/server/GitHubPullRequestProvider.ts", + "import": "./src/server/GitHubPullRequestProvider.ts" + }, + "./server/gitHubPullRequests": { + "types": "./src/server/gitHubPullRequests.ts", + "import": "./src/server/gitHubPullRequests.ts" + }, + "./server/GitHubQuota": { + "types": "./src/server/GitHubQuota.ts", + "import": "./src/server/GitHubQuota.ts" + }, + "./server/gitHubRepositoryResolution": { + "types": "./src/server/gitHubRepositoryResolution.ts", + "import": "./src/server/gitHubRepositoryResolution.ts" + }, + "./server/GitHubSourceControlProvider": { + "types": "./src/server/GitHubSourceControlProvider.ts", + "import": "./src/server/GitHubSourceControlProvider.ts" + }, + "./server/githubStackActions": { + "types": "./src/server/githubStackActions.ts", + "import": "./src/server/githubStackActions.ts" + }, + "./server/githubStackRebase": { + "types": "./src/server/githubStackRebase.ts", + "import": "./src/server/githubStackRebase.ts" + } + }, + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vp test run --config ../../vite.config.ts --dir ." + }, + "dependencies": { + "@t3tools/contracts": "workspace:*", + "@t3tools/provider-core": "workspace:*", + "@t3tools/shared": "workspace:*", + "@t3tools/source-control-core": "workspace:*", + "effect": "catalog:" + }, + "devDependencies": { + "@effect/platform-node": "catalog:", + "@effect/vitest": "catalog:", + "@t3tools/source-control-testing": "workspace:*", + "@types/node": "catalog:", + "vite-plus": "catalog:" + } +} diff --git a/apps/server/src/sourceControl/GitHubApi.test.ts b/packages/source-control-github/src/server/GitHubApi.test.ts similarity index 97% rename from apps/server/src/sourceControl/GitHubApi.test.ts rename to packages/source-control-github/src/server/GitHubApi.test.ts index cd4c2fb384f2..3287ebb8e1a1 100644 --- a/apps/server/src/sourceControl/GitHubApi.test.ts +++ b/packages/source-control-github/src/server/GitHubApi.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "@effect/vitest"; +import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -12,10 +13,10 @@ import { HttpClient, HttpClientResponse, type HttpClientRequest } from "effect/h import * as GitHubApi from "./GitHubApi.ts"; import * as GitHubCredentials from "./GitHubCredentials.ts"; -import * as GitHubQuota from "./githubQuota.ts"; +import * as GitHubQuota from "./GitHubQuota.ts"; import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; -import * as VcsProcess from "../vcs/VcsProcess.ts"; -import * as ServerSettings from "../serverSettings.ts"; +import type * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; +import * as TestSourceControlHost from "@t3tools/source-control-testing/TestSourceControlHost"; const NOW = Date.parse("2026-10-05T12:00:00.000Z"); @@ -439,11 +440,10 @@ describe("GitHubApi", () => { }); describe("GitHubCredentials", () => { - const credentialsWith = (run: VcsProcess.VcsProcess["Service"]["run"]) => + const credentialsWith = (run: SourceControlHost.SourceControlHost["Service"]["process"]["run"]) => GitHubCredentials.layer.pipe( - Layer.provide(Layer.mock(VcsProcess.VcsProcess)({ run })), - Layer.provide(NodeServices.layer), - Layer.provide(ServerSettings.layerTest()), + Layer.provide(TestSourceControlHost.layer({ process: { run } })), + Layer.provide(NodeCrypto.layer), ); it.effect("fails with GitHubCliMissingError when gh is not on PATH", () => diff --git a/apps/server/src/sourceControl/GitHubApi.ts b/packages/source-control-github/src/server/GitHubApi.ts similarity index 98% rename from apps/server/src/sourceControl/GitHubApi.ts rename to packages/source-control-github/src/server/GitHubApi.ts index 1cc66a1323c7..e734e5ff07a5 100644 --- a/apps/server/src/sourceControl/GitHubApi.ts +++ b/packages/source-control-github/src/server/GitHubApi.ts @@ -12,7 +12,7 @@ import { HttpClient, HttpClientRequest, type HttpClientResponse } from "effect/h import { collectUint8StreamText } from "@t3tools/provider-core/server/collectStreamText"; import * as GitHubCredentials from "./GitHubCredentials.ts"; -import * as GitHubQuota from "./githubQuota.ts"; +import * as GitHubQuota from "./GitHubQuota.ts"; import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; const DEFAULT_TIMEOUT = Duration.seconds(30); @@ -30,7 +30,9 @@ export const PinnedGitHubCredential = Context.Reference<{ readonly host: string; readonly token: Redacted.Redacted; readonly credentialFingerprint: string; -} | null>("t3/sourceControl/PinnedGitHubCredential", { defaultValue: () => null }); +} | null>("@t3tools/source-control-github/server/GitHubApi/PinnedGitHubCredential", { + defaultValue: () => null, +}); /** * Set by interactive callers (a user's read or write, not a background sweep). Requests made @@ -38,7 +40,7 @@ export const PinnedGitHubCredential = Context.Reference<{ * pull request should not be refused because a background read exhausted the quota. */ export const AllowGitHubReserve = Context.Reference( - "t3/sourceControl/AllowGitHubReserve", + "@t3tools/source-control-github/server/GitHubApi/AllowGitHubReserve", { defaultValue: () => false }, ); @@ -162,7 +164,7 @@ export class GitHubApi extends Context.Service< GitHubApiError >; } ->()("t3/sourceControl/GitHubApi") {} +>()("@t3tools/source-control-github/server/GitHubApi") {} function normalizeHost(host: string): string { return host.trim().toLowerCase(); diff --git a/apps/server/src/sourceControl/GitHubCredentials.test.ts b/packages/source-control-github/src/server/GitHubCredentials.test.ts similarity index 76% rename from apps/server/src/sourceControl/GitHubCredentials.test.ts rename to packages/source-control-github/src/server/GitHubCredentials.test.ts index 734a309f002f..15ac6bf648a7 100644 --- a/apps/server/src/sourceControl/GitHubCredentials.test.ts +++ b/packages/source-control-github/src/server/GitHubCredentials.test.ts @@ -1,12 +1,14 @@ +import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import { afterEach, beforeEach, describe, expect, it, vi } from "@effect/vitest"; -import * as NodeServices from "@effect/platform-node/NodeServices"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Redacted from "effect/Redacted"; import { ChildProcessSpawner } from "effect/process"; -import * as ServerSettings from "../serverSettings.ts"; -import * as VcsProcess from "../vcs/VcsProcess.ts"; +import { DEFAULT_SERVER_SETTINGS } from "@t3tools/contracts"; +import type * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; +import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings"; +import * as TestSourceControlHost from "@t3tools/source-control-testing/TestSourceControlHost"; import * as GitHubCredentials from "./GitHubCredentials.ts"; const TOKEN_VARIABLES = [ @@ -16,41 +18,51 @@ const TOKEN_VARIABLES = [ "GITHUB_ENTERPRISE_TOKEN", ] as const; +/** Settings keep `enabled` explicit; a host the test leaves it off is enabled. */ +const hostSettings = ( + hosts: Record, +) => + Object.fromEntries( + Object.entries(hosts).map(([host, choice]) => [host, { enabled: true, ...choice }]), + ); + function harness( hosts: Record = {}, signedOut: ReadonlyArray = [], tokens: Record = {}, ) { const calls: Array> = []; - const process = Layer.mock(VcsProcess.VcsProcess)({ - run: (input) => - Effect.sync(() => { - calls.push(input.args); - const user = input.args[input.args.indexOf("--user") + 1]; - if (input.args.includes("--user") && user !== undefined && signedOut.includes(user)) { - return { - exitCode: ChildProcessSpawner.ExitCode(0), - stdout: "", - stderr: "", - stdoutTruncated: false, - stderrTruncated: false, - }; - } + const run: SourceControlHost.SourceControlHost["Service"]["process"]["run"] = (input) => + Effect.sync(() => { + calls.push(input.args); + const user = input.args[input.args.indexOf("--user") + 1]; + if (input.args.includes("--user") && user !== undefined && signedOut.includes(user)) { return { exitCode: ChildProcessSpawner.ExitCode(0), - stdout: input.args.includes("--user") ? `token-for-${user}\n` : "active-token\n", + stdout: "", stderr: "", stdoutTruncated: false, stderrTruncated: false, }; - }), - }); + } + return { + exitCode: ChildProcessSpawner.ExitCode(0), + stdout: input.args.includes("--user") ? `token-for-${user}\n` : "active-token\n", + stderr: "", + stdoutTruncated: false, + stderrTruncated: false, + }; + }); const layer = GitHubCredentials.layer.pipe( Layer.provideMerge( - ServerSettings.ServerSettingsService.layerTest({ github: { hosts, tokens } }), + TestSourceControlHost.layer({ + settings: applyServerSettingsPatch(DEFAULT_SERVER_SETTINGS, { + github: { hosts: hostSettings(hosts), tokens }, + }), + process: { run }, + }), ), - Layer.provide(process), - Layer.provide(NodeServices.layer), + Layer.provide(NodeCrypto.layer), ); return { layer, calls }; } @@ -112,20 +124,20 @@ describe("GitHubCredentials", () => { const { layer, calls } = harness(); return Effect.gen(function* () { const credentials = yield* GitHubCredentials.GitHubCredentials; - const settings = yield* ServerSettings.ServerSettingsService; + const settings = yield* TestSourceControlHost.TestSourceControlHostSettings; expect(Redacted.value((yield* credentials.get("github.com")).token)).toBe("active-token"); - yield* settings.updateSettings({ + yield* settings.update({ github: { hosts: { "github.com": { account: "work", enabled: true } } }, }); expect(Redacted.value((yield* credentials.get("github.com")).token)).toBe("token-for-work"); - yield* settings.updateSettings({ github: { hosts: { "github.com": { enabled: false } } } }); + yield* settings.update({ github: { hosts: { "github.com": { enabled: false } } } }); expect((yield* Effect.flip(credentials.get("github.com")))._tag).toBe( "GitHubHostDisabledError", ); - yield* settings.updateSettings({ github: { hosts: {} } }); + yield* settings.update({ github: { hosts: {} } }); expect(Redacted.value((yield* credentials.get("github.com")).token)).toBe("active-token"); // The unpinned token stayed cached; only the newly pinned account cost a gh call. expect(calls).toHaveLength(2); diff --git a/apps/server/src/sourceControl/GitHubCredentials.ts b/packages/source-control-github/src/server/GitHubCredentials.ts similarity index 96% rename from apps/server/src/sourceControl/GitHubCredentials.ts rename to packages/source-control-github/src/server/GitHubCredentials.ts index d47d57ea7ffe..7483d7e02a52 100644 --- a/apps/server/src/sourceControl/GitHubCredentials.ts +++ b/packages/source-control-github/src/server/GitHubCredentials.ts @@ -13,8 +13,7 @@ import * as Schema from "effect/Schema"; import { HostProcessEnvironment, HostProcessWorkingDirectory } from "@t3tools/shared/hostProcess"; -import * as ServerSettings from "../serverSettings.ts"; -import * as VcsProcess from "../vcs/VcsProcess.ts"; +import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; /** How long a token is reused before `gh` is asked again, so a `gh auth switch` applies soon. */ const TOKEN_TTL = Duration.minutes(5); @@ -94,7 +93,7 @@ export class GitHubCredentials extends Context.Service< /** Drops the held token after GitHub refused it, so the next read asks its source again. */ readonly invalidate: (host: string) => Effect.Effect; } ->()("t3/sourceControl/GitHubCredentials") {} +>()("@t3tools/source-control-github/server/GitHubCredentials") {} function normalizeHost(host: string): string { return host.trim().toLowerCase(); @@ -128,8 +127,8 @@ export function environmentToken( /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { - const process = yield* VcsProcess.VcsProcess; - const serverSettings = yield* ServerSettings.ServerSettingsService; + const sourceControlHost = yield* SourceControlHost.SourceControlHost; + const process = sourceControlHost.process; const crypto = yield* Crypto.Crypto; const environment = yield* HostProcessEnvironment; const workingDirectory = yield* HostProcessWorkingDirectory; @@ -179,14 +178,14 @@ export const make = Effect.gen(function* () { /** The Settings choice for a host; unreadable settings fall back to gh's own choice. */ const hostChoice = (host: string) => - serverSettings.getSettings.pipe( + sourceControlHost.settings.get.pipe( Effect.map((settings) => settings.github.hosts[host]), Effect.orElseSucceed(() => undefined), ); /** A token saved in Settings for the host, read fresh so a saved or removed one applies at once. */ const savedToken = (host: string) => - serverSettings.getSettings.pipe( + sourceControlHost.settings.get.pipe( Effect.map((settings) => settings.github.tokens[host]?.trim() || null), Effect.orElseSucceed(() => null), ); diff --git a/apps/server/src/pullRequest/GitHubPullRequestApi.test.ts b/packages/source-control-github/src/server/GitHubPullRequestApi.test.ts similarity index 99% rename from apps/server/src/pullRequest/GitHubPullRequestApi.test.ts rename to packages/source-control-github/src/server/GitHubPullRequestApi.test.ts index 8888f4c11f45..badb79677585 100644 --- a/apps/server/src/pullRequest/GitHubPullRequestApi.test.ts +++ b/packages/source-control-github/src/server/GitHubPullRequestApi.test.ts @@ -12,12 +12,12 @@ import * as Redacted from "effect/Redacted"; import { HttpClient, HttpClientResponse } from "effect/http"; -import { AllowGitHubReserve } from "../sourceControl/GitHubApi.ts"; -import * as GitHubApi from "../sourceControl/GitHubApi.ts"; -import * as GitHubCredentials from "../sourceControl/GitHubCredentials.ts"; -import * as GitHubQuota from "../sourceControl/githubQuota.ts"; +import { AllowGitHubReserve } from "./GitHubApi.ts"; +import * as GitHubApi from "./GitHubApi.ts"; +import * as GitHubCredentials from "./GitHubCredentials.ts"; +import * as GitHubQuota from "./GitHubQuota.ts"; import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; -import * as VcsProcess from "../vcs/VcsProcess.ts"; +import * as TestSourceControlHost from "@t3tools/source-control-testing/TestSourceControlHost"; import * as GitHubPullRequestApi from "./GitHubPullRequestApi.ts"; import { KnownWorkflowRuns } from "./gitHubConditionalChecks.ts"; @@ -103,7 +103,8 @@ const layer = it.layer( Layer.provideMerge(mockApi), Layer.provideMerge(GitHubQuota.layer), Layer.provide(NodeCrypto.layer), - Layer.provide(VcsProcess.layer.pipe(Layer.provideMerge(NodeServices.layer))), + Layer.provide(TestSourceControlHost.layer()), + Layer.provide(NodeServices.layer), ), ); @@ -509,7 +510,7 @@ it.effect( Layer.provide(Layer.mergeAll(credentials, http)), Layer.provide(GitHubQuota.layer), Layer.provide(SourceControlRateLimit.layer), - Layer.merge(VcsProcess.layer), + Layer.merge(TestSourceControlHost.layer()), Layer.provideMerge(NodeServices.layer), ), ), diff --git a/apps/server/src/pullRequest/GitHubPullRequestApi.ts b/packages/source-control-github/src/server/GitHubPullRequestApi.ts similarity index 99% rename from apps/server/src/pullRequest/GitHubPullRequestApi.ts rename to packages/source-control-github/src/server/GitHubPullRequestApi.ts index dd093df00b77..ceff9c6c0b5c 100644 --- a/apps/server/src/pullRequest/GitHubPullRequestApi.ts +++ b/packages/source-control-github/src/server/GitHubPullRequestApi.ts @@ -42,9 +42,9 @@ import { type PullRequestPreview, } from "@t3tools/contracts"; -import * as GitHubApi from "../sourceControl/GitHubApi.ts"; -import { readGraphQlPages } from "../sourceControl/githubGraphQl.ts"; -import * as VcsProcess from "../vcs/VcsProcess.ts"; +import * as GitHubApi from "./GitHubApi.ts"; +import { readGraphQlPages } from "./githubGraphQl.ts"; +import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; import { ACTOR_AVATARS_GRAPHQL_QUERY, @@ -793,7 +793,7 @@ export class GitHubPullRequestApi extends Context.Service< readonly body: string; }) => Effect.Effect; } ->()("t3/pullRequest/GitHubPullRequestApi") {} +>()("@t3tools/source-control-github/server/GitHubPullRequestApi") {} /** * The GraphQL API takes owner and name as separate arguments, so `owner/repo` is split here. @@ -1105,7 +1105,7 @@ const SIMPLE_ACTION_MUTATIONS = { /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { const api = yield* GitHubApi.GitHubApi; - const vcsProcess = yield* VcsProcess.VcsProcess; + const sourceControlHost = yield* SourceControlHost.SourceControlHost; const fileSystem = yield* FileSystem.FileSystem; const revalidateChecks = yield* makeChecksRevalidator; const routingIdentities = new Map< @@ -2497,7 +2497,7 @@ export const make = Effect.gen(function* () { if (input.stackNumber !== undefined) return runGitHubStackAction({ ...input, stackNumber: input.stackNumber }).pipe( Effect.provideService(GitHubApi.GitHubApi, api), - Effect.provideService(VcsProcess.VcsProcess, vcsProcess), + Effect.provideService(SourceControlHost.SourceControlHost, sourceControlHost), Effect.provideService(FileSystem.FileSystem, fileSystem), ); if (input.action === "revert") { diff --git a/apps/server/src/pullRequest/GitHubPullRequestProvider.test.ts b/packages/source-control-github/src/server/GitHubPullRequestProvider.test.ts similarity index 99% rename from apps/server/src/pullRequest/GitHubPullRequestProvider.test.ts rename to packages/source-control-github/src/server/GitHubPullRequestProvider.test.ts index df4f7da4cd2c..98e8aeb5c15b 100644 --- a/apps/server/src/pullRequest/GitHubPullRequestProvider.test.ts +++ b/packages/source-control-github/src/server/GitHubPullRequestProvider.test.ts @@ -6,7 +6,7 @@ import * as Result from "effect/Result"; import type { PullRequestReaction } from "@t3tools/contracts"; import { decodePullRequestDetailJson } from "./gitHubPullRequestJson.ts"; -import * as GitHubApi from "../sourceControl/GitHubApi.ts"; +import * as GitHubApi from "./GitHubApi.ts"; import * as GitHubPullRequestApi from "./GitHubPullRequestApi.ts"; import type { GitHubPullRequestCore } from "./gitHubPullRequestJson.ts"; import { gitHubViewerPermissions, loginAvatarUrl, make } from "./GitHubPullRequestProvider.ts"; diff --git a/apps/server/src/pullRequest/GitHubPullRequestProvider.ts b/packages/source-control-github/src/server/GitHubPullRequestProvider.ts similarity index 99% rename from apps/server/src/pullRequest/GitHubPullRequestProvider.ts rename to packages/source-control-github/src/server/GitHubPullRequestProvider.ts index 67f871501ddb..a3030f6c86ad 100644 --- a/apps/server/src/pullRequest/GitHubPullRequestProvider.ts +++ b/packages/source-control-github/src/server/GitHubPullRequestProvider.ts @@ -7,7 +7,7 @@ import type { PullRequestViewerPermissions, } from "@t3tools/contracts"; -import * as GitHubApi from "../sourceControl/GitHubApi.ts"; +import * as GitHubApi from "./GitHubApi.ts"; import * as GitHubPullRequestApi from "./GitHubPullRequestApi.ts"; import { PullRequestProviderError, diff --git a/apps/server/src/sourceControl/githubQuota.test.ts b/packages/source-control-github/src/server/GitHubQuota.test.ts similarity index 98% rename from apps/server/src/sourceControl/githubQuota.test.ts rename to packages/source-control-github/src/server/GitHubQuota.test.ts index e9c1fac93eb0..4c2c707cde7d 100644 --- a/apps/server/src/sourceControl/githubQuota.test.ts +++ b/packages/source-control-github/src/server/GitHubQuota.test.ts @@ -2,7 +2,7 @@ import { assert, describe, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as TestClock from "effect/testing/TestClock"; -import * as GitHubQuota from "./githubQuota.ts"; +import * as GitHubQuota from "./GitHubQuota.ts"; import { CredentialScope } from "@t3tools/source-control-core/server/SourceControlRateLimit"; const RESET = Date.parse("2099-08-13T14:00:00Z"); diff --git a/apps/server/src/sourceControl/githubQuota.ts b/packages/source-control-github/src/server/GitHubQuota.ts similarity index 98% rename from apps/server/src/sourceControl/githubQuota.ts rename to packages/source-control-github/src/server/GitHubQuota.ts index 642c48fddc9a..5dcd5469b9ed 100644 --- a/apps/server/src/sourceControl/githubQuota.ts +++ b/packages/source-control-github/src/server/GitHubQuota.ts @@ -70,7 +70,7 @@ export class GitHubQuota extends Context.Service< headers: Readonly>, ) => Effect.Effect; } ->()("t3/sourceControl/githubQuota") {} +>()("@t3tools/source-control-github/server/GitHubQuota") {} const make = Effect.gen(function* () { const snapshots = yield* Ref.make>(new Map()); diff --git a/apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts b/packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts similarity index 98% rename from apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts rename to packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts index 602acc9d1e27..fee5d5e9546c 100644 --- a/apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts +++ b/packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts @@ -10,8 +10,8 @@ import * as Schema from "effect/Schema"; import { ChildProcessSpawner } from "effect/process"; import * as TestClock from "effect/testing/TestClock"; -import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; -import * as VcsProcess from "../vcs/VcsProcess.ts"; +import type * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; +import * as TestSourceControlHost from "@t3tools/source-control-testing/TestSourceControlHost"; import * as GitHubApi from "./GitHubApi.ts"; import * as GitHubCredentials from "./GitHubCredentials.ts"; import { parseGitHubAuthStatus } from "./gitHubAuthStatus.ts"; @@ -25,7 +25,7 @@ const processResult = ( readonly stderr?: string; readonly exitCode?: ChildProcessSpawner.ExitCode; }, -): VcsProcess.VcsProcessOutput => ({ +): SourceControlHost.SourceControlProcessOutput => ({ exitCode: options?.exitCode ?? ChildProcessSpawner.ExitCode(0), stdout, stderr: options?.stderr ?? "", @@ -33,7 +33,10 @@ const processResult = ( stderrTruncated: false, }); -const processOutput = (stdout: string, exitCode = 0): VcsProcess.VcsProcessOutput => ({ +const processOutput = ( + stdout: string, + exitCode = 0, +): SourceControlHost.SourceControlProcessOutput => ({ exitCode: ChildProcessSpawner.ExitCode(exitCode), stdout, stderr: "", @@ -87,7 +90,7 @@ function harness(input: { git.push([name, args]); return value; }); - const driver = Layer.mock(GitVcsDriver.GitVcsDriver)({ + const hostGit: Partial = { execute: (args) => Effect.sync(() => { git.push(["execute", args.args]); @@ -108,18 +111,20 @@ function harness(input: { ? Effect.succeed([...(input.localBranches ?? [])]) : Effect.fail(input.gitFailure as never), resolveCommit: () => Effect.succeed({ commitSha: "abc123" }), - }); - const process = Layer.mock(VcsProcess.VcsProcess)({ - run: (args) => - Effect.succeed( - args.args[0] === "remote" - ? processOutput(input.remotes) - : processOutput(input.resolved ?? "", input.resolved ? 0 : 1), - ), + }; + const host = TestSourceControlHost.layer({ + git: hostGit, + process: { + run: (args) => + Effect.succeed( + args.args[0] === "remote" + ? processOutput(input.remotes) + : processOutput(input.resolved ?? "", input.resolved ? 0 : 1), + ), + }, }); const layer = Layer.mergeAll( - driver, - process, + host, Layer.mock(GitHubApi.GitHubApi)(input.api), NodeServices.layer, ); diff --git a/apps/server/src/sourceControl/GitHubSourceControlProvider.ts b/packages/source-control-github/src/server/GitHubSourceControlProvider.ts similarity index 98% rename from apps/server/src/sourceControl/GitHubSourceControlProvider.ts rename to packages/source-control-github/src/server/GitHubSourceControlProvider.ts index 35876794048a..0f442f65d712 100644 --- a/apps/server/src/sourceControl/GitHubSourceControlProvider.ts +++ b/packages/source-control-github/src/server/GitHubSourceControlProvider.ts @@ -22,8 +22,6 @@ import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; import { decodeJsonResult } from "@t3tools/shared/schemaJson"; import { isSshRemoteUrl } from "@t3tools/shared/sourceControl"; -import * as ServerSettings from "../serverSettings.ts"; -import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as GitHubApi from "./GitHubApi.ts"; import { decodeGitHubPullRequestEntries, @@ -54,7 +52,7 @@ import { type SourceControlCliDiscoverySpec, type SourceControlManagedCliDiscoverySpec, } from "@t3tools/source-control-core/server/discovery"; -import * as VcsProcess from "../vcs/VcsProcess.ts"; +import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; const decodeLinkSubject = Schema.decodeUnknownEffect( Schema.fromJsonString( @@ -192,9 +190,9 @@ function environmentTokenVariable(environment: NodeJS.ProcessEnv): string | null */ export const makeDiscovery = Effect.gen(function* () { const api = yield* GitHubApi.GitHubApi; - const process = yield* VcsProcess.VcsProcess; + const sourceControlHost = yield* SourceControlHost.SourceControlHost; + const process = sourceControlHost.process; const environment = yield* HostProcessEnvironment; - const serverSettings = yield* ServerSettings.ServerSettingsService; return { type: "managed-cli", @@ -202,7 +200,7 @@ export const makeDiscovery = Effect.gen(function* () { label: discovery.label, installHint: discovery.installHint, probe: Effect.fn("GitHubSourceControlProvider.discovery")(function* (cwd: string) { - const settings = yield* serverSettings.getSettings.pipe( + const settings = yield* sourceControlHost.settings.get.pipe( Effect.map((current) => current.github), Effect.orElseSucceed(() => DEFAULT_SERVER_SETTINGS.github), ); @@ -477,9 +475,10 @@ const contextHost = (context: SourceControlProvider.SourceControlProviderContext export const make = Effect.gen(function* () { const api = yield* GitHubApi.GitHubApi; - const process = yield* VcsProcess.VcsProcess; + const sourceControlHost = yield* SourceControlHost.SourceControlHost; + const process = sourceControlHost.process; const environment = yield* HostProcessEnvironment; - const git = yield* GitVcsDriver.GitVcsDriver; + const git = sourceControlHost.git; const fileSystem = yield* FileSystem.FileSystem; const gitRead = (cwd: string, args: ReadonlyArray) => diff --git a/packages/source-control-github/src/server/driver.ts b/packages/source-control-github/src/server/driver.ts new file mode 100644 index 000000000000..d57ba07b2017 --- /dev/null +++ b/packages/source-control-github/src/server/driver.ts @@ -0,0 +1,20 @@ +/** + * The GitHub driver: repository operations and discovery through `gh` and git, and pull requests + * through the GitHub REST and GraphQL APIs. + * + * @module source-control-github/server/driver + */ +import { defineSourceControlDriver } from "@t3tools/source-control-core/server/driver"; +import * as Effect from "effect/Effect"; + +import * as GitHubPullRequestProvider from "./GitHubPullRequestProvider.ts"; +import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; + +export const driver = defineSourceControlDriver({ + kind: "github", + make: Effect.all({ + sourceControl: GitHubSourceControlProvider.make, + discovery: GitHubSourceControlProvider.makeDiscovery, + pullRequests: GitHubPullRequestProvider.make, + }), +}); diff --git a/apps/server/src/sourceControl/gitHubAuthStatus.ts b/packages/source-control-github/src/server/gitHubAuthStatus.ts similarity index 100% rename from apps/server/src/sourceControl/gitHubAuthStatus.ts rename to packages/source-control-github/src/server/gitHubAuthStatus.ts diff --git a/apps/server/src/pullRequest/gitHubConditionalChecks.test.ts b/packages/source-control-github/src/server/gitHubConditionalChecks.test.ts similarity index 99% rename from apps/server/src/pullRequest/gitHubConditionalChecks.test.ts rename to packages/source-control-github/src/server/gitHubConditionalChecks.test.ts index 79048b1d7453..a621e0e9ebb0 100644 --- a/apps/server/src/pullRequest/gitHubConditionalChecks.test.ts +++ b/packages/source-control-github/src/server/gitHubConditionalChecks.test.ts @@ -6,7 +6,7 @@ import * as Schema from "effect/Schema"; import * as Redacted from "effect/Redacted"; import type { PullRequestCheck } from "@t3tools/contracts"; -import * as GitHubApi from "../sourceControl/GitHubApi.ts"; +import * as GitHubApi from "./GitHubApi.ts"; import { KnownWorkflowRuns, makeChecksRevalidator } from "./gitHubConditionalChecks.ts"; const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); diff --git a/apps/server/src/pullRequest/gitHubConditionalChecks.ts b/packages/source-control-github/src/server/gitHubConditionalChecks.ts similarity index 97% rename from apps/server/src/pullRequest/gitHubConditionalChecks.ts rename to packages/source-control-github/src/server/gitHubConditionalChecks.ts index 353089ebf0f9..afeb14a0935e 100644 --- a/apps/server/src/pullRequest/gitHubConditionalChecks.ts +++ b/packages/source-control-github/src/server/gitHubConditionalChecks.ts @@ -7,7 +7,7 @@ import * as Schema from "effect/Schema"; import * as Semaphore from "effect/Semaphore"; import { PositiveInt, type PullRequestChecks } from "@t3tools/contracts"; -import * as GitHubApi from "../sourceControl/GitHubApi.ts"; +import * as GitHubApi from "./GitHubApi.ts"; import type { GitHubPullRequestDetail } from "./gitHubPullRequestJson.ts"; import type { GitHubPullRequestApiError } from "./GitHubPullRequestApi.ts"; import type { ProviderRepositoryRef } from "@t3tools/source-control-core/server/PullRequestProvider"; @@ -43,7 +43,9 @@ export type KnownWorkflowRun = typeof WorkflowRunSchema.Type; export const KnownWorkflowRuns = Context.Reference<{ readonly headSha: string; readonly runs: ReadonlyArray; -} | null>("t3/pullRequest/KnownWorkflowRuns", { defaultValue: () => null }); +} | null>("@t3tools/source-control-github/server/gitHubConditionalChecks/KnownWorkflowRuns", { + defaultValue: () => null, +}); type Validator = { etag: string | undefined; next: boolean; body: string }; diff --git a/apps/server/src/pullRequest/gitHubPullRequestJson.test.ts b/packages/source-control-github/src/server/gitHubPullRequestJson.test.ts similarity index 100% rename from apps/server/src/pullRequest/gitHubPullRequestJson.test.ts rename to packages/source-control-github/src/server/gitHubPullRequestJson.test.ts diff --git a/apps/server/src/pullRequest/gitHubPullRequestJson.ts b/packages/source-control-github/src/server/gitHubPullRequestJson.ts similarity index 99% rename from apps/server/src/pullRequest/gitHubPullRequestJson.ts rename to packages/source-control-github/src/server/gitHubPullRequestJson.ts index edca11f5a0d0..956f1ef5f4ad 100644 --- a/apps/server/src/pullRequest/gitHubPullRequestJson.ts +++ b/packages/source-control-github/src/server/gitHubPullRequestJson.ts @@ -35,7 +35,7 @@ import type { import { quoteGitPatchPath } from "@t3tools/shared/gitPatchPath"; import { decodeJsonResult } from "@t3tools/shared/schemaJson"; -import { aliasedGraphQlDocument, type GraphQlDocument } from "../sourceControl/githubGraphQl.ts"; +import { aliasedGraphQlDocument, type GraphQlDocument } from "./githubGraphQl.ts"; import { dedupeChecks } from "@t3tools/source-control-core/server/pullRequestChecks"; /** diff --git a/apps/server/src/sourceControl/gitHubPullRequests.ts b/packages/source-control-github/src/server/gitHubPullRequests.ts similarity index 100% rename from apps/server/src/sourceControl/gitHubPullRequests.ts rename to packages/source-control-github/src/server/gitHubPullRequests.ts diff --git a/apps/server/src/sourceControl/gitHubRepositoryResolution.test.ts b/packages/source-control-github/src/server/gitHubRepositoryResolution.test.ts similarity index 100% rename from apps/server/src/sourceControl/gitHubRepositoryResolution.test.ts rename to packages/source-control-github/src/server/gitHubRepositoryResolution.test.ts diff --git a/apps/server/src/sourceControl/gitHubRepositoryResolution.ts b/packages/source-control-github/src/server/gitHubRepositoryResolution.ts similarity index 100% rename from apps/server/src/sourceControl/gitHubRepositoryResolution.ts rename to packages/source-control-github/src/server/gitHubRepositoryResolution.ts diff --git a/apps/server/src/sourceControl/githubGraphQl.test.ts b/packages/source-control-github/src/server/githubGraphQl.test.ts similarity index 100% rename from apps/server/src/sourceControl/githubGraphQl.test.ts rename to packages/source-control-github/src/server/githubGraphQl.test.ts diff --git a/apps/server/src/sourceControl/githubGraphQl.ts b/packages/source-control-github/src/server/githubGraphQl.ts similarity index 100% rename from apps/server/src/sourceControl/githubGraphQl.ts rename to packages/source-control-github/src/server/githubGraphQl.ts diff --git a/apps/server/src/pullRequest/githubStackActions.test.ts b/packages/source-control-github/src/server/githubStackActions.test.ts similarity index 95% rename from apps/server/src/pullRequest/githubStackActions.test.ts rename to packages/source-control-github/src/server/githubStackActions.test.ts index 7600b9dc9a26..846d88332a60 100644 --- a/apps/server/src/pullRequest/githubStackActions.test.ts +++ b/packages/source-control-github/src/server/githubStackActions.test.ts @@ -7,8 +7,8 @@ import * as TestClock from "effect/testing/TestClock"; import * as FileSystem from "effect/FileSystem"; import * as Redacted from "effect/Redacted"; import { ChildProcessSpawner } from "effect/process"; -import * as GitHubApi from "../sourceControl/GitHubApi.ts"; -import * as VcsProcess from "../vcs/VcsProcess.ts"; +import * as GitHubApi from "./GitHubApi.ts"; +import * as TestSourceControlHost from "@t3tools/source-control-testing/TestSourceControlHost"; import { runGitHubStackAction as runStackAction } from "./githubStackActions.ts"; /** @@ -24,20 +24,22 @@ type Send = ( /** Every git command the cascade ran, in order; none actually runs. */ let gitCalls: Array> = []; const fakeGit = Layer.mergeAll( - Layer.mock(VcsProcess.VcsProcess)({ - run: (input) => - Effect.sync(() => { - gitCalls.push(input.args); - const stdout = - input.args[0] === "rev-parse" || input.args[0] === "merge-base" ? "new-sha\n" : ""; - return { - exitCode: ChildProcessSpawner.ExitCode(0), - stdout, - stderr: "", - stdoutTruncated: false, - stderrTruncated: false, - }; - }), + TestSourceControlHost.layer({ + process: { + run: (input) => + Effect.sync(() => { + gitCalls.push(input.args); + const stdout = + input.args[0] === "rev-parse" || input.args[0] === "merge-base" ? "new-sha\n" : ""; + return { + exitCode: ChildProcessSpawner.ExitCode(0), + stdout, + stderr: "", + stdoutTruncated: false, + stderrTruncated: false, + }; + }), + }, }), FileSystem.layerNoop({ makeTempDirectoryScoped: () => Effect.succeed("/tmp/scratch") }), ); diff --git a/apps/server/src/pullRequest/githubStackActions.ts b/packages/source-control-github/src/server/githubStackActions.ts similarity index 98% rename from apps/server/src/pullRequest/githubStackActions.ts rename to packages/source-control-github/src/server/githubStackActions.ts index c2e8fadb751d..a6d90711620f 100644 --- a/apps/server/src/pullRequest/githubStackActions.ts +++ b/packages/source-control-github/src/server/githubStackActions.ts @@ -8,8 +8,8 @@ import * as Clock from "effect/Clock"; import * as Result from "effect/Result"; import * as Schema from "effect/Schema"; -import * as GitHubApi from "../sourceControl/GitHubApi.ts"; -import { aliasedGraphQlDocument } from "../sourceControl/githubGraphQl.ts"; +import * as GitHubApi from "./GitHubApi.ts"; +import { aliasedGraphQlDocument } from "./githubGraphQl.ts"; import { decodePullRequestStacksJson } from "./gitHubPullRequestJson.ts"; import { cascadeRebaseStack } from "./githubStackRebase.ts"; diff --git a/apps/server/src/pullRequest/githubStackRebase.ts b/packages/source-control-github/src/server/githubStackRebase.ts similarity index 96% rename from apps/server/src/pullRequest/githubStackRebase.ts rename to packages/source-control-github/src/server/githubStackRebase.ts index 2cd2b5f88bb7..a1dffa05d976 100644 --- a/apps/server/src/pullRequest/githubStackRebase.ts +++ b/packages/source-control-github/src/server/githubStackRebase.ts @@ -4,8 +4,8 @@ import * as Redacted from "effect/Redacted"; import * as Schema from "effect/Schema"; import * as Base64 from "effect/encoding/Base64"; -import * as GitHubApi from "../sourceControl/GitHubApi.ts"; -import * as VcsProcess from "../vcs/VcsProcess.ts"; +import * as GitHubApi from "./GitHubApi.ts"; +import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; /** One open layer of a stack, bottom to top, with the head the reader reviewed. */ export interface CascadeLayer { @@ -58,7 +58,7 @@ export const cascadeRebaseStack = Effect.fn("cascadeRebaseStack")(function* (inp readonly remote?: string; }) { const api = yield* GitHubApi.GitHubApi; - const process = yield* VcsProcess.VcsProcess; + const { process } = yield* SourceControlHost.SourceControlHost; const fileSystem = yield* FileSystem.FileSystem; const { token } = yield* api.credential(input.host); const first = input.layers[0]; diff --git a/packages/source-control-github/tsconfig.json b/packages/source-control-github/tsconfig.json new file mode 100644 index 000000000000..374bac55202d --- /dev/null +++ b/packages/source-control-github/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["src"] +} diff --git a/packages/source-control-testing/package.json b/packages/source-control-testing/package.json index 66426fe03377..60c1bbcf43bf 100644 --- a/packages/source-control-testing/package.json +++ b/packages/source-control-testing/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@t3tools/contracts": "workspace:*", + "@t3tools/shared": "workspace:*", "@t3tools/source-control-core": "workspace:*", "effect": "catalog:" }, diff --git a/packages/source-control-testing/src/TestSourceControlHost.ts b/packages/source-control-testing/src/TestSourceControlHost.ts index eee1a96f13a9..a4cb30db7341 100644 --- a/packages/source-control-testing/src/TestSourceControlHost.ts +++ b/packages/source-control-testing/src/TestSourceControlHost.ts @@ -1,15 +1,23 @@ /** - * A `SourceControlHost.SourceControlHost` for provider tests. Settings are fixed unless the test - * supplies its own. Process runs and git operations go to what the test supplies; anything else - * dies, so an unexpected CLI or git call is visible. + * A `SourceControlHost.SourceControlHost` for provider tests. Settings start from the defaults + * or what the test supplies, and `TestSourceControlHostSettings` patches them mid-test the way a + * client's settings write would. Process runs and git operations go to what the test supplies; + * anything else dies, so an unexpected CLI or git call is visible. * * @module source-control-testing/TestSourceControlHost */ -import { DEFAULT_SERVER_SETTINGS, type ServerSettings } from "@t3tools/contracts"; +import { + DEFAULT_SERVER_SETTINGS, + type ServerSettings, + type ServerSettingsPatch, +} from "@t3tools/contracts"; +import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings"; import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; +import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; +import * as Ref from "effect/Ref"; export interface TestSourceControlHostOptions { readonly settings?: ServerSettings; @@ -17,12 +25,20 @@ export interface TestSourceControlHostOptions { readonly git?: Partial; } +/** Lets a test change the settings its `layer` reports. */ +export class TestSourceControlHostSettings extends Context.Service< + TestSourceControlHostSettings, + { readonly update: (patch: ServerSettingsPatch) => Effect.Effect } +>()("@t3tools/source-control-testing/TestSourceControlHost/TestSourceControlHostSettings") {} + type Git = SourceControlHost.SourceControlHost["Service"]["git"]; const unexpectedGit = (operation: string) => () => Effect.die(`Unexpected git ${operation} in a test that supplied none.`); const failingGit: Git = { + execute: unexpectedGit("execute"), + resolveCommit: unexpectedGit("resolveCommit"), remotes: unexpectedGit("remotes"), readConfigValue: unexpectedGit("readConfigValue"), resolvePrimaryRemoteName: unexpectedGit("resolvePrimaryRemoteName"), @@ -36,17 +52,25 @@ const failingGit: Git = { export const layer = ( options: TestSourceControlHostOptions = {}, -): Layer.Layer => - Layer.succeed( - SourceControlHost.SourceControlHost, - SourceControlHost.SourceControlHost.of({ - settings: { get: Effect.succeed(options.settings ?? DEFAULT_SERVER_SETTINGS) }, - process: { - run: - options.process?.run ?? - ((input) => Effect.die(`Unexpected ${input.command} run in ${input.operation}.`)), - }, - git: { ...failingGit, ...options.git }, +): Layer.Layer => + Layer.effectContext( + Effect.gen(function* () { + const settings = yield* Ref.make(options.settings ?? DEFAULT_SERVER_SETTINGS); + const host = SourceControlHost.SourceControlHost.of({ + settings: { get: Ref.get(settings) }, + process: { + run: + options.process?.run ?? + ((input) => Effect.die(`Unexpected ${input.command} run in ${input.operation}.`)), + }, + git: { ...failingGit, ...options.git }, + }); + return Context.make(SourceControlHost.SourceControlHost, host).pipe( + Context.add(TestSourceControlHostSettings, { + update: (patch) => + Ref.update(settings, (current) => applyServerSettingsPatch(current, patch)), + }), + ); }), ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ad28290d985..cdee534c2d30 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -660,6 +660,9 @@ importers: '@t3tools/source-control-forgejo': specifier: workspace:* version: link:../../packages/source-control-forgejo + '@t3tools/source-control-github': + specifier: workspace:* + version: link:../../packages/source-control-github '@t3tools/source-control-gitlab': specifier: workspace:* version: link:../../packages/source-control-gitlab @@ -1640,6 +1643,40 @@ importers: specifier: 1.0.0 version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + packages/source-control-github: + dependencies: + '@t3tools/contracts': + specifier: workspace:* + version: link:../contracts + '@t3tools/provider-core': + specifier: workspace:* + version: link:../provider-core + '@t3tools/shared': + specifier: workspace:* + version: link:../shared + '@t3tools/source-control-core': + specifier: workspace:* + version: link:../source-control-core + effect: + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) + devDependencies: + '@effect/platform-node': + specifier: 4.0.2 + version: 4.0.2(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(redis@6.2.1)(utf-8-validate@6.0.6) + '@effect/vitest': + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + '@t3tools/source-control-testing': + specifier: workspace:* + version: link:../source-control-testing + '@types/node': + specifier: 24.12.4 + version: 24.12.4 + vite-plus: + specifier: 1.0.0 + version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + packages/source-control-gitlab: dependencies: '@t3tools/contracts': @@ -1673,6 +1710,9 @@ importers: '@t3tools/contracts': specifier: workspace:* version: link:../contracts + '@t3tools/shared': + specifier: workspace:* + version: link:../shared '@t3tools/source-control-core': specifier: workspace:* version: link:../source-control-core diff --git a/scripts/release-smoke.ts b/scripts/release-smoke.ts index 96b1df41e1ea..7bc6da481716 100644 --- a/scripts/release-smoke.ts +++ b/scripts/release-smoke.ts @@ -41,6 +41,7 @@ const workspaceFiles = [ "packages/source-control-bitbucket/package.json", "packages/source-control-core/package.json", "packages/source-control-forgejo/package.json", + "packages/source-control-github/package.json", "packages/source-control-gitlab/package.json", "packages/source-control-testing/package.json", "packages/effect-acp/package.json", From e5dca2332fed450c6f1b3c7472666fbe80531c5a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 15:26:18 -0700 Subject: [PATCH 16/39] refactor(usage): transcript readers come from their drivers (#17576) Co-authored-by: Claude Opus 5.5 --- .../src/provider/Drivers/ClaudeDriver.ts | 4 +- .../src/provider/Drivers/CodexDriver.ts | 4 +- .../src/provider/Drivers/claudeUsage.test.ts | 74 +++ .../src/provider/Drivers/claudeUsage.ts | 120 ++++ .../src/provider/Drivers/codexUsage.test.ts | 193 ++++++ .../server/src/provider/Drivers/codexUsage.ts | 265 ++++++++ apps/server/src/provider/builtInDrivers.ts | 20 +- apps/server/src/usage/UsageService.test.ts | 4 +- apps/server/src/usage/UsageService.ts | 179 +++--- .../src/usage/antigravityUsageReader.ts | 2 +- .../src/usage/cursorAccountCache.test.ts | 2 +- apps/server/src/usage/cursorAccountCache.ts | 2 +- apps/server/src/usage/cursorUsageReader.ts | 2 +- apps/server/src/usage/opencodeUsageReader.ts | 2 +- .../server/src/usage/usageAggregation.test.ts | 2 +- apps/server/src/usage/usageAggregation.ts | 2 +- apps/server/src/usage/usagePricing.test.ts | 2 +- apps/server/src/usage/usagePricing.ts | 2 +- apps/server/src/usage/usageScanCache.test.ts | 45 +- apps/server/src/usage/usageScanCache.ts | 103 +-- apps/server/src/usage/usageTestFormats.ts | 21 + .../src/usage/usageTranscriptReader.test.ts | 33 +- .../server/src/usage/usageTranscriptReader.ts | 146 ++--- .../usage/usageTranscriptStreaming.test.ts | 8 +- .../server/src/usage/usageTranscripts.test.ts | 598 ------------------ apps/server/src/usage/usageTranscripts.ts | 530 ---------------- packages/provider-core/package.json | 4 + packages/provider-core/src/server/driver.ts | 14 +- .../provider-core/src/server/usage.test.ts | 17 + packages/provider-core/src/server/usage.ts | 179 ++++++ packages/provider-grok/package.json | 4 + packages/provider-grok/src/server/driver.ts | 4 +- .../provider-grok/src/server/usage.test.ts | 316 +++++++++ packages/provider-grok/src/server/usage.ts | 239 +++++++ 34 files changed, 1731 insertions(+), 1411 deletions(-) create mode 100644 apps/server/src/provider/Drivers/claudeUsage.test.ts create mode 100644 apps/server/src/provider/Drivers/claudeUsage.ts create mode 100644 apps/server/src/provider/Drivers/codexUsage.test.ts create mode 100644 apps/server/src/provider/Drivers/codexUsage.ts create mode 100644 apps/server/src/usage/usageTestFormats.ts delete mode 100644 apps/server/src/usage/usageTranscripts.test.ts delete mode 100644 apps/server/src/usage/usageTranscripts.ts create mode 100644 packages/provider-core/src/server/usage.test.ts create mode 100644 packages/provider-core/src/server/usage.ts create mode 100644 packages/provider-grok/src/server/usage.test.ts create mode 100644 packages/provider-grok/src/server/usage.ts diff --git a/apps/server/src/provider/Drivers/ClaudeDriver.ts b/apps/server/src/provider/Drivers/ClaudeDriver.ts index f7935bb8d481..e9f37580215b 100644 --- a/apps/server/src/provider/Drivers/ClaudeDriver.ts +++ b/apps/server/src/provider/Drivers/ClaudeDriver.ts @@ -34,6 +34,7 @@ import { type ClaudeAdapterV2DriverEnv, } from "../../orchestration-v2/Adapters/ClaudeAdapterV2.ts"; import { ProviderDriverError } from "../Errors.ts"; +import { claudeUsageReader } from "./claudeUsage.ts"; import { makeClaudeScopedLimitNames } from "../claudeUsageLimits.ts"; import * as ClaudeResetCredits from "../claudeResetCredits.ts"; import * as ResetCreditCoordinator from "../resetCreditCoordinator.ts"; @@ -107,7 +108,7 @@ export type ClaudeDriverEnv = | ProviderEventLoggers.ProviderEventLoggers | ServerConfig.ServerConfig; -export const ClaudeDriver: ProviderDriver = { +export const ClaudeDriver: ProviderDriver = { driverKind: DRIVER_KIND, metadata: { displayName: "Claude", @@ -115,6 +116,7 @@ export const ClaudeDriver: ProviderDriver = { }, configSchema: ClaudeSettings, defaultConfig: (): ClaudeSettings => decodeClaudeSettings({}), + usage: claudeUsageReader, create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; diff --git a/apps/server/src/provider/Drivers/CodexDriver.ts b/apps/server/src/provider/Drivers/CodexDriver.ts index 660c0dad965a..32ffed8905b4 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.ts @@ -73,6 +73,7 @@ import { resolveCodexHomeLayout, } from "./CodexHomeLayout.ts"; import { makeManagedCodexProvider } from "./CodexManagedProvider.ts"; +import { codexUsageReader } from "./codexUsage.ts"; import * as CodexInstallation from "../CodexInstallation.ts"; import * as ServerSecretStore from "../../auth/ServerSecretStore.ts"; import * as ServerEnvironment from "../../environment/ServerEnvironment.ts"; @@ -125,7 +126,7 @@ export type CodexDriverEnv = | ServerEnvironment.ServerEnvironmentIdentity | CodexInstallation.CodexInstallation; -export const CodexDriver: ProviderDriver = { +export const CodexDriver: ProviderDriver = { driverKind: DRIVER_KIND, metadata: { displayName: "Codex", @@ -133,6 +134,7 @@ export const CodexDriver: ProviderDriver = { }, configSchema: CodexSettings, defaultConfig: (): CodexSettings => decodeCodexSettings({}), + usage: codexUsageReader, create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => Effect.gen(function* () { if (config.setupMode === "managed") diff --git a/apps/server/src/provider/Drivers/claudeUsage.test.ts b/apps/server/src/provider/Drivers/claudeUsage.test.ts new file mode 100644 index 000000000000..8c2d08c36e2a --- /dev/null +++ b/apps/server/src/provider/Drivers/claudeUsage.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from "@effect/vitest"; + +import { parseClaudeLine } from "./claudeUsage.ts"; + +/** Shaped after a real Claude Code assistant record. */ +function claudeLine(overrides: { + messageId: string; + contentType: string; + model?: string; + outputTokens?: number; + speed?: string; +}): string { + return JSON.stringify({ + type: "assistant", + timestamp: "2026-08-07T04:05:13.944Z", + sessionId: "5a128faa-8253-489e-b935-6c08e8e670c0", + cwd: "/home/theo/project", + message: { + id: overrides.messageId, + role: "assistant", + model: overrides.model ?? "claude-fable-5", + content: [{ type: overrides.contentType }], + usage: { + input_tokens: 2, + cache_creation_input_tokens: 66818, + cache_read_input_tokens: 1000, + output_tokens: overrides.outputTokens ?? 286, + ...(overrides.speed === undefined ? {} : { speed: overrides.speed }), + }, + }, + }); +} + +describe("parseClaudeLine", () => { + it("extracts token totals and a dedupe key", () => { + const record = parseClaudeLine(claudeLine({ messageId: "msg_1", contentType: "text" })); + + expect(record).not.toBeNull(); + expect(record?.provider).toBe("claude"); + expect(record?.model).toBe("claude-fable-5"); + expect(record?.totals).toEqual({ + uncachedInputTokens: 2, + cachedInputTokens: 1000, + cacheCreationTokens: 66818, + outputTokens: 286, + reasoningTokens: 0, + }); + expect(record?.dedupeKey).toBe("msg_1:"); + expect(record?.speed).toBe("standard"); + }); + + it("marks fast-mode requests", () => { + const line = (speed: string) => + parseClaudeLine(claudeLine({ messageId: "msg_1", contentType: "text", speed })); + + expect(line("fast")?.speed).toBe("fast"); + expect(line("standard")?.speed).toBe("standard"); + }); + + it("gives every content block of one message the same dedupe key", () => { + // T3 Code writes one record per content block, each repeating the parent + // message's full usage. Summing them would overcount ~2.4x on real data. + const text = parseClaudeLine(claudeLine({ messageId: "msg_2", contentType: "text" })); + const toolUse = parseClaudeLine(claudeLine({ messageId: "msg_2", contentType: "tool_use" })); + + expect(text?.dedupeKey).toBe(toolUse?.dedupeKey); + expect(text?.totals).toEqual(toolUse?.totals); + }); + + it("ignores records that are not assistant messages", () => { + expect(parseClaudeLine(JSON.stringify({ type: "user", message: {} }))).toBeNull(); + expect(parseClaudeLine("not json")).toBeNull(); + }); +}); diff --git a/apps/server/src/provider/Drivers/claudeUsage.ts b/apps/server/src/provider/Drivers/claudeUsage.ts new file mode 100644 index 000000000000..155ebc6a33ec --- /dev/null +++ b/apps/server/src/provider/Drivers/claudeUsage.ts @@ -0,0 +1,120 @@ +/** + * Usage history for Claude Code: the transcript format of its + * `/projects/**.jsonl` session files and where each instance keeps them. + * + * @module provider/Drivers/claudeUsage + */ +import * as NodeOS from "node:os"; + +import type { ClaudeSettings } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Path from "effect/Path"; + +import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; +import { + parseTimestampMs, + tokenCount, + type ProviderUsageReader, + type TranscriptUsageFormat, + type UsageRecord, +} from "@t3tools/provider-core/server/usage"; + +/** + * Parses one line of a Claude Code transcript. + * + * T3 Code writes one record per assistant *content block*, and every one of + * those records repeats the same complete `usage` object for the parent + * message. Summing them overcounts by roughly 2.4x on a real workload, so the + * caller must drop repeats by `dedupeKey` and keep the first. + */ +export function parseClaudeLine(line: string): UsageRecord | null { + let parsed: unknown; + try { + parsed = JSON.parse(line); + } catch { + return null; + } + return parseClaudeRecord(parsed); +} + +function parseClaudeRecord(parsed: unknown): UsageRecord | null { + if (typeof parsed !== "object" || parsed === null) return null; + + const record = parsed as Record; + if (record["type"] !== "assistant") return null; + + const message = record["message"]; + if (typeof message !== "object" || message === null) return null; + const messageRecord = message as Record; + + const usage = messageRecord["usage"]; + if (typeof usage !== "object" || usage === null) return null; + const usageRecord = usage as Record; + + const timestampMs = parseTimestampMs(record["timestamp"]); + if (timestampMs === null) return null; + + const model = typeof messageRecord["model"] === "string" ? messageRecord["model"] : ""; + if (model.length === 0) return null; + + const messageId = typeof messageRecord["id"] === "string" ? messageRecord["id"] : null; + const requestId = typeof record["requestId"] === "string" ? record["requestId"] : null; + // Matches ccusage: prefer the message/request pair, fall back to whichever + // half exists. Records with neither cannot be de-duplicated. + const dedupeKey = + messageId === null && requestId === null ? null : `${messageId ?? ""}:${requestId ?? ""}`; + + const cost = record["costUSD"]; + + return { + provider: "claude", + timestampMs, + model, + sessionId: typeof record["sessionId"] === "string" ? record["sessionId"] : "", + totals: { + uncachedInputTokens: tokenCount(usageRecord["input_tokens"]), + cachedInputTokens: tokenCount(usageRecord["cache_read_input_tokens"]), + cacheCreationTokens: tokenCount(usageRecord["cache_creation_input_tokens"]), + outputTokens: tokenCount(usageRecord["output_tokens"]), + // Anthropic folds thinking tokens into output and does not break them out. + reasoningTokens: 0, + }, + reportedCostUsd: typeof cost === "number" && Number.isFinite(cost) ? cost : null, + speed: usageRecord["speed"] === "fast" ? "fast" : "standard", + dedupeKey, + }; +} + +const orEmpty = (record: UsageRecord | null): readonly UsageRecord[] => + record === null ? [] : [record]; + +export const claudeUsageFormat: TranscriptUsageFormat = { + // Keeps the dedupe and cost metadata. The `usage` subtree keeps future token fields. + selectFields: { + type: true, + timestamp: true, + requestId: true, + sessionId: true, + costUSD: true, + message: { id: true, model: true, usage: true }, + }, + mightCarryUsage: (line) => line.includes('"usage"'), + parseLine: (line) => orEmpty(parseClaudeLine(line)), + parseProjected: (projected) => orEmpty(parseClaudeRecord(projected)), +}; + +export const claudeUsageReader: ProviderUsageReader = { + kind: "transcripts", + provider: "claude", + format: claudeUsageFormat, + directories: Effect.fn("claudeUsageReader.directories")(function* ({ config, environment }) { + // An undecodable config has no trustworthy home to read. + if (config === undefined) return []; + const path = yield* Path.Path; + const configured = config.homePath.trim(); + const home = configured + ? expandHomePath(configured) + : environment.CLAUDE_CONFIG_DIR?.trim() || path.join(NodeOS.homedir(), ".claude"); + return [{ dir: path.resolve(home, "projects") }]; + }), +}; diff --git a/apps/server/src/provider/Drivers/codexUsage.test.ts b/apps/server/src/provider/Drivers/codexUsage.test.ts new file mode 100644 index 000000000000..36f15dbdf468 --- /dev/null +++ b/apps/server/src/provider/Drivers/codexUsage.test.ts @@ -0,0 +1,193 @@ +import { describe, expect, it } from "@effect/vitest"; + +import { initialCodexScanState, parseCodexLine } from "./codexUsage.ts"; + +describe("parseCodexLine", () => { + const sessionMeta = JSON.stringify({ + type: "session_meta", + timestamp: "2026-08-01T05:17:41.289Z", + payload: { type: "session_meta", id: "019fbbc1-b12c-7360-a685-28c181f0025f" }, + }); + const turnContext = JSON.stringify({ + type: "turn_context", + timestamp: "2026-08-01T05:17:42.694Z", + payload: { type: "turn_context", model: "gpt-5.6-sol" }, + }); + const tokenCount = (inputTokens: number, cached: number, output: number, reasoning: number) => + JSON.stringify({ + type: "event_msg", + timestamp: "2026-08-01T05:17:49.919Z", + payload: { + type: "token_count", + info: { + last_token_usage: { + input_tokens: inputTokens, + cached_input_tokens: cached, + cache_write_input_tokens: 0, + output_tokens: output, + reasoning_output_tokens: reasoning, + }, + }, + }, + }); + + it("attributes usage to the model from the preceding turn context", () => { + const state = initialCodexScanState(); + parseCodexLine(sessionMeta, state); + parseCodexLine(turnContext, state); + const record = parseCodexLine(tokenCount(19239, 11008, 299, 116), state); + + expect(record?.provider).toBe("codex"); + expect(record?.model).toBe("gpt-5.6-sol"); + expect(record?.sessionId).toBe("019fbbc1-b12c-7360-a685-28c181f0025f"); + // Codex reports input_tokens inclusive of the cached portion. + expect(record?.totals.uncachedInputTokens).toBe(19239 - 11008); + expect(record?.totals.cachedInputTokens).toBe(11008); + expect(record?.totals.reasoningTokens).toBe(116); + }); + + it("skips a repeated token_count so deltas are not double counted", () => { + const state = initialCodexScanState(); + parseCodexLine(turnContext, state); + const first = parseCodexLine(tokenCount(100, 0, 10, 0), state); + const repeat = parseCodexLine(tokenCount(100, 0, 10, 0), state); + + expect(first).not.toBeNull(); + expect(repeat).toBeNull(); + }); + + it("drops usage that arrives before any model is known", () => { + const state = initialCodexScanState(); + expect(parseCodexLine(tokenCount(100, 0, 10, 0), state)).toBeNull(); + }); + + it("does not let a pre-model event poison the duplicate signature", () => { + // A token_count before its turn_context is dropped; the identical event + // re-emitted once the model is known must still be counted. + const state = initialCodexScanState(); + expect(parseCodexLine(tokenCount(100, 0, 10, 0), state)).toBeNull(); + parseCodexLine(turnContext, state); + expect(parseCodexLine(tokenCount(100, 0, 10, 0), state)).not.toBeNull(); + }); + + it("carries the service tier from the latest thread settings", () => { + const settings = (thread_settings: Record) => + JSON.stringify({ + type: "event_msg", + timestamp: "2026-08-01T05:17:42.000Z", + payload: { type: "thread_settings_applied", thread_settings }, + }); + const state = initialCodexScanState(); + parseCodexLine(turnContext, state); + const speedAfter = (line: string, output: number) => { + parseCodexLine(line, state); + return parseCodexLine(tokenCount(100, 0, output, 0), state)?.speed; + }; + + expect(parseCodexLine(tokenCount(100, 0, 1, 0), state)?.speed).toBe("standard"); + expect(speedAfter(settings({ service_tier: "ultrafast" }), 2)).toBe("ultrafast"); + expect(speedAfter(settings({ service_tier: "priority" }), 3)).toBe("fast"); + // Codex omits the field when no tier was requested. + expect(speedAfter(settings({ model: "gpt-6-astra" }), 4)).toBe("standard"); + }); + + // A forked/subagent rollout opens with the parent's history copied in and + // every line re-stamped to the fork instant, then the ancestors' session + // metas. Counting those again multiplied usage ~1.85x on real data (#5758). + describe("forked rollouts", () => { + const meta = (overrides: { + id: string; + timestamp: string; + forkedFromId?: string; + spawnParentId?: string; + }) => + JSON.stringify({ + type: "session_meta", + timestamp: overrides.timestamp, + payload: { + type: "session_meta", + id: overrides.id, + ...(overrides.forkedFromId === undefined + ? {} + : { forked_from_id: overrides.forkedFromId }), + ...(overrides.spawnParentId === undefined + ? {} + : { + source: { + subagent: { thread_spawn: { parent_thread_id: overrides.spawnParentId } }, + }, + }), + }, + }); + const stamped = (timestamp: string, line: string) => { + const parsed = JSON.parse(line) as { timestamp: string }; + parsed.timestamp = timestamp; + return JSON.stringify(parsed); + }; + + it("keeps the child session id over copied ancestor metas", () => { + const state = initialCodexScanState(); + parseCodexLine(meta({ id: "child", timestamp: "2026-08-01T05:00:00.000Z" }), state); + parseCodexLine(meta({ id: "parent", timestamp: "2026-08-01T05:00:00.000Z" }), state); + parseCodexLine(turnContext, state); + const record = parseCodexLine(tokenCount(100, 0, 10, 0), state); + + expect(record?.sessionId).toBe("child"); + }); + + it("drops the re-stamped copied burst and keeps the first real event", () => { + const state = initialCodexScanState(); + const forkInstant = "2026-08-01T05:00:00.000Z"; + parseCodexLine(meta({ id: "child", timestamp: forkInstant, forkedFromId: "parent" }), state); + parseCodexLine(meta({ id: "parent", timestamp: forkInstant }), state); + parseCodexLine(stamped(forkInstant, turnContext), state); + + // Copied history: written in one burst at the fork instant. + expect( + parseCodexLine(stamped("2026-08-01T05:00:00.001Z", tokenCount(100, 0, 10, 0)), state), + ).toBeNull(); + expect( + parseCodexLine(stamped("2026-08-01T05:00:00.002Z", tokenCount(200, 0, 20, 0)), state), + ).toBeNull(); + + // The child's first genuine turn lands seconds later and must count. + const real = parseCodexLine( + stamped("2026-08-01T05:00:06.000Z", tokenCount(300, 0, 30, 0)), + state, + ); + expect(real).not.toBeNull(); + expect(real?.totals.outputTokens).toBe(30); + + // Suppression never restarts, even for closely spaced later events. + const next = parseCodexLine( + stamped("2026-08-01T05:00:06.100Z", tokenCount(400, 0, 40, 0)), + state, + ); + expect(next).not.toBeNull(); + }); + + it("recognizes subagent spawns without forked_from_id", () => { + const state = initialCodexScanState(); + const spawnInstant = "2026-08-01T05:00:00.000Z"; + parseCodexLine( + meta({ id: "child", timestamp: spawnInstant, spawnParentId: "parent" }), + state, + ); + parseCodexLine(stamped(spawnInstant, turnContext), state); + expect( + parseCodexLine(stamped("2026-08-01T05:00:00.001Z", tokenCount(100, 0, 10, 0)), state), + ).toBeNull(); + }); + + it("does not suppress anything in a rollout that is not a fork", () => { + const state = initialCodexScanState(); + parseCodexLine(meta({ id: "root", timestamp: "2026-08-01T05:00:00.000Z" }), state); + parseCodexLine(stamped("2026-08-01T05:00:00.100Z", turnContext), state); + const record = parseCodexLine( + stamped("2026-08-01T05:00:00.200Z", tokenCount(100, 0, 10, 0)), + state, + ); + expect(record).not.toBeNull(); + }); + }); +}); diff --git a/apps/server/src/provider/Drivers/codexUsage.ts b/apps/server/src/provider/Drivers/codexUsage.ts new file mode 100644 index 000000000000..fc5294e20597 --- /dev/null +++ b/apps/server/src/provider/Drivers/codexUsage.ts @@ -0,0 +1,265 @@ +/** + * Usage history for Codex: the rollout format of `/sessions/**.jsonl` + * and where each instance's shared home keeps them. + * + * @module provider/Drivers/codexUsage + */ +import type { CodexSettings } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; + +import { + parseTimestampMs, + tokenCount, + totalTokens, + type ProviderUsageReader, + type TranscriptUsageFormat, + type UsageRecord, + type UsageSpeed, +} from "@t3tools/provider-core/server/usage"; +import type { UsageTokenTotals } from "@t3tools/contracts"; + +import { resolveCodexHomeLayout } from "./CodexHomeLayout.ts"; + +/** + * Rolling state for a single Codex rollout file. + * + * Codex `token_count` events carry no model or service tier, so both are + * carried forward: the model from the most recent `turn_context`, the tier from + * the most recent `thread_settings_applied`. Sessions that switch either + * mid-run attribute correctly from the switch onward. + * + * Persisted in the scan cache at each resume point. The field order is the + * stored JSON's, so keep it stable. + */ +export const CodexScanState = Schema.Struct({ + model: Schema.mutableKey(Schema.String), + speed: Schema.mutableKey(Schema.Literals(["standard", "fast", "ultrafast"])), + sessionId: Schema.mutableKey(Schema.String), + lastUsageSignature: Schema.mutableKey(Schema.NullOr(Schema.String)), + sawSessionMeta: Schema.mutableKey(Schema.Boolean), + /** While true, leading usage events are re-stamped copies of parent history. */ + suppressingForkCopies: Schema.mutableKey(Schema.Boolean), + forkCopyAnchorMs: Schema.mutableKey(Schema.Finite), +}); +export type CodexScanState = typeof CodexScanState.Type; + +export function initialCodexScanState(): CodexScanState { + return { + model: "", + speed: "standard", + sessionId: "", + lastUsageSignature: null, + sawSessionMeta: false, + suppressingForkCopies: false, + forkCopyAnchorMs: 0, + }; +} + +/** + * A forked or subagent rollout opens with the parent's full history copied in, + * every line re-stamped to the fork instant. Those copies are written in one + * synchronous burst (observed gaps 0-40ms), while the child's first genuine + * usage event only lands after a real model turn (observed 5s+). One second of + * separation splits the two cleanly; `ccusage` uses the same threshold. + */ +const FORK_COPY_MAX_GAP_MS = 1000; + +/** Whether a `session_meta` payload marks the rollout as a fork or subagent. */ +function isForkedSessionMeta(payload: Record): boolean { + if (typeof payload["forked_from_id"] === "string") return true; + const source = payload["source"]; + if (typeof source !== "object" || source === null) return false; + const subagent = (source as Record)["subagent"]; + if (typeof subagent !== "object" || subagent === null) return false; + const spawn = (subagent as Record)["thread_spawn"]; + if (typeof spawn !== "object" || spawn === null) return false; + return typeof (spawn as Record)["parent_thread_id"] === "string"; +} + +/** + * Feeds one line of a Codex rollout into `state`, returning a record when the + * line was a usage event. + * + * Deltas come from `last_token_usage`. Summing those across a session + * reconciles with the session's final `total_token_usage`, provided + * consecutive duplicate events are dropped, which this does. + */ +export function parseCodexLine(line: string, state: CodexScanState): UsageRecord | null { + let parsed: unknown; + try { + parsed = JSON.parse(line); + } catch { + return null; + } + return parseCodexRecord(parsed, state); +} + +function parseCodexRecord(parsed: unknown, state: CodexScanState): UsageRecord | null { + if (typeof parsed !== "object" || parsed === null) return null; + + const record = parsed as Record; + const payload = record["payload"]; + if (typeof payload !== "object" || payload === null) return null; + const payloadRecord = payload as Record; + const payloadType = payloadRecord["type"]; + + if (record["type"] === "session_meta") { + // Only the first meta describes this file's own session. A forked rollout + // repeats the ancestors' metas right after it; letting those through would + // reassign every subsequent record to an ancestor session. + if (state.sawSessionMeta) return null; + state.sawSessionMeta = true; + const id = payloadRecord["id"] ?? payloadRecord["session_id"]; + if (typeof id === "string") state.sessionId = id; + const metaTimestampMs = parseTimestampMs(record["timestamp"]); + if (metaTimestampMs !== null && isForkedSessionMeta(payloadRecord)) { + state.suppressingForkCopies = true; + state.forkCopyAnchorMs = metaTimestampMs; + } + return null; + } + + if (record["type"] === "turn_context") { + if (typeof payloadRecord["model"] === "string") state.model = payloadRecord["model"]; + return null; + } + + if (payloadType === "thread_settings_applied") { + const settings = payloadRecord["thread_settings"]; + if (typeof settings === "object" && settings !== null) { + state.speed = codexSpeed((settings as Record)["service_tier"]); + } + return null; + } + + if (payloadType !== "token_count") return null; + + const info = payloadRecord["info"]; + if (typeof info !== "object" || info === null) return null; + const last = (info as Record)["last_token_usage"]; + if (typeof last !== "object" || last === null) return null; + const lastRecord = last as Record; + + // Only an event that is otherwise eligible may consume the duplicate + // signature. A token_count arriving before its turn_context (no model yet) + // must not poison it, or the re-emitted copy after the model is known would + // be skipped as a duplicate and those tokens never counted. + const timestampMs = parseTimestampMs(record["timestamp"]); + if (timestampMs === null) return null; + if (state.model.length === 0) return null; + + // Codex re-emits an unchanged token_count on some stream boundaries. Summing + // those would double count, so identical consecutive payloads are skipped. + const signature = JSON.stringify(lastRecord); + if (signature === state.lastUsageSignature) return null; + state.lastUsageSignature = signature; + + // In a forked rollout the copied parent history was already counted from the + // parent's own file. Drop the leading burst; the first usage event separated + // from its predecessor by a real turn's worth of time ends it for good. + if (state.suppressingForkCopies) { + if (timestampMs - state.forkCopyAnchorMs < FORK_COPY_MAX_GAP_MS) { + state.forkCopyAnchorMs = timestampMs; + return null; + } + state.suppressingForkCopies = false; + } + + const inputTokens = tokenCount(lastRecord["input_tokens"]); + const cachedInputTokens = tokenCount(lastRecord["cached_input_tokens"]); + const cacheCreationTokens = tokenCount(lastRecord["cache_write_input_tokens"]); + const outputTokens = tokenCount(lastRecord["output_tokens"]); + + const totals: UsageTokenTotals = { + // Codex reports `input_tokens` inclusive of the cached portion. + uncachedInputTokens: Math.max(0, inputTokens - cachedInputTokens - cacheCreationTokens), + cachedInputTokens, + cacheCreationTokens, + outputTokens, + // Reported inside output_tokens, surfaced separately for the token mix. + reasoningTokens: Math.min(outputTokens, tokenCount(lastRecord["reasoning_output_tokens"])), + }; + + if (totalTokens(totals) === 0) return null; + + return { + provider: "codex", + timestampMs, + model: state.model, + sessionId: state.sessionId, + totals, + // Codex does not report cost in the rollout. + reportedCostUsd: null, + speed: state.speed, + // Events surviving the fork-copy suppression above are unique to this + // rollout, so they need no global dedup. + dedupeKey: null, + }; +} + +/** + * Maps a Codex `service_tier` to its billing speed. Codex omits the field when + * no tier was requested, which bills as standard, as do `default` and + * `standard`. `fast` is accepted as an alias of `priority`. + */ +function codexSpeed(serviceTier: unknown): UsageSpeed { + if (serviceTier === "priority" || serviceTier === "fast") return "fast"; + if (serviceTier === "ultrafast") return "ultrafast"; + return "standard"; +} + +const orEmpty = (record: UsageRecord | null): readonly UsageRecord[] => + record === null ? [] : [record]; + +export const codexUsageFormat: TranscriptUsageFormat = { + // Keeps the fields the reducer reads as well as the usage itself. + selectFields: { + type: true, + timestamp: true, + payload: { + type: true, + id: true, + session_id: true, + model: true, + thread_settings: { service_tier: true }, + forked_from_id: true, + source: { subagent: { thread_spawn: { parent_thread_id: true } } }, + info: { last_token_usage: true }, + }, + }, + // `turn_context`, `thread_settings_applied` and `session_meta` lines hold no + // usage, but they carry the model, tier and session the reducer needs. + mightCarryUsage: (line) => + line.includes('"token_count"') || + line.includes('"turn_context"') || + line.includes('"thread_settings_applied"') || + line.includes('"session_meta"'), + parseLine: (line, state) => orEmpty(parseCodexLine(line, state)), + parseProjected: (projected, state) => orEmpty(parseCodexRecord(projected, state)), + state: { initial: initialCodexScanState, schema: CodexScanState }, + // A rollout that moved after it was read leaves a copy in both places. + sharedSessionsAcrossFiles: true, +}; + +export const codexUsageReader: ProviderUsageReader = { + kind: "transcripts", + provider: "codex", + format: codexUsageFormat, + directories: Effect.fn("codexUsageReader.directories")(function* ({ config, environment }) { + // An undecodable config has no trustworthy home to read. + if (config === undefined) return []; + const path = yield* Path.Path; + const environmentHome = environment.CODEX_HOME?.trim(); + const layout = yield* resolveCodexHomeLayout( + config.setupMode !== "managed" && + !config.homePath.trim() && + !config.shadowHomePath.trim() && + environmentHome + ? { ...config, homePath: environmentHome } + : config, + ); + return [{ dir: path.resolve(layout.sharedHomePath, "sessions") }]; + }), +}; diff --git a/apps/server/src/provider/builtInDrivers.ts b/apps/server/src/provider/builtInDrivers.ts index 845ffcf137c4..4cecbc6ca50b 100644 --- a/apps/server/src/provider/builtInDrivers.ts +++ b/apps/server/src/provider/builtInDrivers.ts @@ -32,7 +32,10 @@ import { GrokDriver, type GrokDriverEnv } from "@t3tools/provider-grok/server"; import { OpenCodeDriver, type OpenCodeDriverEnv } from "@t3tools/provider-opencode/server"; import { MuseDriver, type MuseDriverEnv } from "@t3tools/provider-muse/server"; import { PiDriver, type PiDriverEnv } from "@t3tools/provider-pi/server"; -import type { AnyProviderDriver } from "@t3tools/provider-core/server/driver"; +import type { + AnyProviderDriver, + ProviderUsageReaderEnv, +} from "@t3tools/provider-core/server/driver"; /** * Union of infrastructure services required to construct any built-in @@ -66,3 +69,18 @@ export const BUILT_IN_DRIVERS: ReadonlyArray + | ProviderUsageReaderEnv + | ProviderUsageReaderEnv; + +/** + * The drivers that keep usage history, in the order the usage page reads + * them: transcript readers first, then scan readers. Aggregation keeps the + * first copy of a duplicate record, so the order is part of the result. + */ +export const BUILT_IN_USAGE_DRIVERS: ReadonlyArray< + AnyProviderDriver +> = [ClaudeDriver, CodexDriver, GrokDriver]; diff --git a/apps/server/src/usage/UsageService.test.ts b/apps/server/src/usage/UsageService.test.ts index 52e98f0eaf7d..7e98a5467779 100644 --- a/apps/server/src/usage/UsageService.test.ts +++ b/apps/server/src/usage/UsageService.test.ts @@ -33,7 +33,7 @@ import { HttpClient, HttpClientResponse } from "effect/http"; import * as ServerConfig from "../config.ts"; import * as ServerSettings from "../serverSettings.ts"; import * as CursorUsageReader from "./cursorUsageReader.ts"; -import type { UsageRecord } from "./usageTranscripts.ts"; +import type { UsageRecord } from "@t3tools/provider-core/server/usage"; import * as UsageService from "./UsageService.ts"; const encodeUnknownJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); @@ -805,6 +805,8 @@ describe("UsageService", () => { }, [ProviderInstanceId.make("grok-work")]: { driver: ProviderDriverKind.make("grok"), + // An undecodable config must not hide history Grok reads by home alone. + config: { customModels: "not-a-list" }, environment: [{ name: "GROK_HOME", value: grokHome, sensitive: false }], }, }, diff --git a/apps/server/src/usage/UsageService.ts b/apps/server/src/usage/UsageService.ts index 76bdf21f377a..94d967286f45 100644 --- a/apps/server/src/usage/UsageService.ts +++ b/apps/server/src/usage/UsageService.ts @@ -21,8 +21,6 @@ import * as NodeOS from "node:os"; import { - ClaudeSettings, - CodexSettings, type ProviderInstanceConfig, ProviderInstanceId, USAGE_CONTRACT_VERSION, @@ -56,9 +54,15 @@ import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite"; import * as ServerConfig from "../config.ts"; import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; import * as ServerSettings from "../serverSettings.ts"; -import { resolveCodexHomeLayout } from "../provider/Drivers/CodexHomeLayout.ts"; import { resolveAntigravityInstanceDirectories } from "../provider/antigravityAuthSupport.ts"; +import { BUILT_IN_USAGE_DRIVERS, type BuiltInUsageReadersEnv } from "../provider/builtInDrivers.ts"; +import type { ProviderDriver } from "@t3tools/provider-core/server/driver"; import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; +import type { + ProviderUsageInstance, + TranscriptUsageFormat, + UsageRecord, +} from "@t3tools/provider-core/server/usage"; import { readOpenCodeUsage } from "./opencodeUsageReader.ts"; import { makeAntigravityUsageCache, readAntigravityUsage } from "./antigravityUsageReader.ts"; import { @@ -90,7 +94,6 @@ import { type CachedFile, type ScanCache, } from "./usageScanCache.ts"; -import type { UsageRecord } from "./usageTranscripts.ts"; const LITELLM_RATES_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"; @@ -119,8 +122,24 @@ const CURSOR_ACCOUNT_READ_ERROR = "Cursor account usage could not be read."; /** Transcripts parsed at once. More gains little once the disk stays busy. */ const TRANSCRIPT_READ_CONCURRENCY = 4; -const decodeCodexSettings = Schema.decodeOption(CodexSettings); -const decodeClaudeSettings = Schema.decodeOption(ClaudeSettings); +/** The transcript readers, in driver order. */ +const transcriptReaders = BUILT_IN_USAGE_DRIVERS.flatMap((driver) => + driver.usage?.kind === "transcripts" ? [{ driver, reader: driver.usage }] : [], +); + +/** Transcript formats by provider, for decoding the persisted scan cache. */ +const transcriptFormats = new Map( + transcriptReaders.map(({ reader }) => [reader.provider, reader.format] as const), +); + +/** One transcript directory to scan. */ +interface TranscriptSource { + readonly provider: UsageProviderKind; + readonly format: TranscriptUsageFormat; + readonly dir: string; + readonly volumeId: string; + readonly fileName?: string; +} /** On-disk shape of the rate snapshot. */ const RatesCacheFile = Schema.Struct({ @@ -145,13 +164,21 @@ function isLaterRead(a: CachedFile, b: CachedFile): boolean { return a.mtimeMs > b.mtimeMs || (a.mtimeMs === b.mtimeMs && a.size > b.size); } +/** Providers whose format lets one session's records appear in several files. */ +const sharedSessionProviders: ReadonlySet = new Set( + transcriptReaders.flatMap(({ reader }) => + reader.format.sharedSessionsAcrossFiles ? [reader.provider] : [], + ), +); + /** - * Codex sessions with records in more than one file, such as a rollout that - * moved after it was read. Only these need cross-file dedupe keys: within one - * file the occurrence count already keeps every key unique, so keying the rest - * would only build and hash a string for each of their records. + * Sessions of `sharedSessionProviders` with records in more than one file, + * such as a rollout that moved after it was read. Only these need cross-file + * dedupe keys: within one file the occurrence count already keeps every key + * unique, so keying the rest would only build and hash a string for each of + * their records. */ -function sharedCodexSessions( +function sharedSessions( files: readonly { readonly records: readonly UsageRecord[] }[], ): ReadonlySet { const firstFile = new Map(); @@ -159,7 +186,8 @@ function sharedCodexSessions( for (const [index, file] of files.entries()) { let previous = ""; for (const { provider, sessionId } of file.records) { - if (provider !== "codex" || sessionId === previous || sessionId.length === 0) continue; + if (!sharedSessionProviders.has(provider) || sessionId === previous || sessionId.length === 0) + continue; previous = sessionId; const first = firstFile.get(sessionId); if (first === undefined) firstFile.set(sessionId, index); @@ -218,6 +246,8 @@ export const make = Effect.gen(function* () { const hostEnvironment = yield* HostProcessEnvironment; const platform = yield* HostProcessPlatform; const cursorAccountReader = yield* CursorUsageReader.CursorAccountReader; + // The readers yield their own services; scans run them against this context. + const readerContext = yield* Effect.context(); const fileCache: ScanCache = new Map(); const antigravityCache = makeAntigravityUsageCache(); @@ -338,60 +368,45 @@ export const make = Effect.gen(function* () { ), ); - /** Resolves the transcript directory for each provider. */ + /** + * Every instance of `driver` with its decoded config, or none when it does + * not decode. Disabled accounts still have history. An unconfigured default + * slot runs with default config, just as it does in the provider registry. + */ + const usageInstances = ( + driver: ProviderDriver, + settings: ServerSettingsValue, + ): Array> => { + const entries: Array< + readonly [ProviderInstanceId, Pick, boolean] + > = Object.entries(settings.providerInstances) + .filter(([, instance]) => instance.driver === driver.driverKind) + .map(([id, instance]) => [ProviderInstanceId.make(id), instance, true] as const); + if (!Object.hasOwn(settings.providerInstances, driver.driverKind)) { + entries.push([ProviderInstanceId.make(driver.driverKind), {}, false]); + } + const decodeConfig = Schema.decodeUnknownOption(driver.configSchema); + return entries.map(([instanceId, instance, configured]) => ({ + instanceId, + config: Option.getOrUndefined(decodeConfig(instance.config ?? {})), + environment: mergeProviderInstanceEnvironment(instance.environment, hostEnvironment), + configured, + })); + }; + + /** Resolves every transcript directory the usage readers point at. */ const resolveTranscriptDirs = Effect.fn("UsageService.resolveTranscriptDirs")(function* ( settings: ServerSettingsValue, retentionCutoffMs: number, ) { - const dirs: Array<{ - provider: UsageProviderKind; - dir: string; - volumeId: string; - fileName?: string; - }> = []; + const dirs: Array = []; const seen = new Set(); - for (const driver of ["claudeAgent", "codex", "grok"] as const) { - // Disabled accounts still have history. An unconfigured default slot - // runs with default config, just as it does in the provider registry. - const instances: Array< - Pick & { instanceId: ProviderInstanceId } - > = Object.entries(settings.providerInstances) - .filter(([, instance]) => instance.driver === driver) - .map(([id, instance]) => ({ ...instance, instanceId: ProviderInstanceId.make(id) })); - if (!Object.hasOwn(settings.providerInstances, driver)) { - instances.push({ instanceId: ProviderInstanceId.make(driver) }); - } - for (const instance of instances) { - const environment = mergeProviderInstanceEnvironment(instance.environment, hostEnvironment); - const provider = driver === "claudeAgent" ? "claude" : driver; - let home: string; - if (driver === "codex") { - const decoded = decodeCodexSettings(instance.config ?? {}); - if (Option.isNone(decoded)) continue; - const codexConfig = decoded.value; - const environmentHome = environment.CODEX_HOME?.trim(); - const layout = yield* resolveCodexHomeLayout( - codexConfig.setupMode !== "managed" && - !codexConfig.homePath.trim() && - !codexConfig.shadowHomePath.trim() && - environmentHome - ? { ...codexConfig, homePath: environmentHome } - : codexConfig, - ); - home = layout.sharedHomePath; - } else if (driver === "claudeAgent") { - const decoded = decodeClaudeSettings(instance.config ?? {}); - if (Option.isNone(decoded)) continue; - const configured = decoded.value.homePath.trim(); - home = configured - ? expandHomePath(configured) - : environment.CLAUDE_CONFIG_DIR?.trim() || path.join(NodeOS.homedir(), ".claude"); - } else { - home = expandHomePath( - environment.GROK_HOME?.trim() || path.join(NodeOS.homedir(), ".grok"), - ); - } - const directory = path.resolve(home, provider === "claude" ? "projects" : "sessions"); + for (const { driver, reader } of transcriptReaders) { + const { provider, format } = reader; + const directories = yield* Effect.forEach(usageInstances(driver, settings), (instance) => + reader.directories(instance), + ); + for (const { dir: directory, fileName } of directories.flat()) { const sourceKey = provider + "\0" + directory; const previous = sourceCache.get(sourceKey); // Keep canonical paths and source fingerprints stable after root cleanup, @@ -423,9 +438,10 @@ export const make = Effect.gen(function* () { seen.add(key); dirs.push({ provider, + format, dir, volumeId, - ...(provider === "grok" ? { fileName: "updates.jsonl" } : {}), + ...(fileName === undefined ? {} : { fileName }), }); } } @@ -456,7 +472,9 @@ export const make = Effect.gen(function* () { cacheDirty = document !== null; } if (document === null) return; - for (const [path, entry] of decodeScanCache(document)) fileCache.set(path, entry); + for (const [path, entry] of decodeScanCache(document, transcriptFormats)) { + fileCache.set(path, entry); + } const sources = decodeCachedSources(document); if (Option.isSome(sources)) { for (const [key, source] of Object.entries(sources.value.sources)) @@ -536,6 +554,7 @@ export const make = Effect.gen(function* () { size: number, mtimeMs: number, provider: UsageProviderKind, + format: TranscriptUsageFormat, ): Effect.Effect<{ readonly records: readonly UsageRecord[]; readonly update?: { readonly entry: CachedFile; readonly replaces: CachedFile | undefined }; @@ -566,7 +585,7 @@ export const make = Effect.gen(function* () { : undefined; const parsed = yield* Effect.promise(() => - readTranscriptRecords(filePath, provider, resumeFrom), + readTranscriptRecords(filePath, format, resumeFrom), ); // A read failure is not an empty transcript: caching it under this // (size, mtime) would silently drop the file's usage until it changes. @@ -611,15 +630,10 @@ export const make = Effect.gen(function* () { } const scanTranscriptDir = Effect.fn("UsageService.scanTranscriptDir")(function* ( - source: { - readonly provider: UsageProviderKind; - readonly dir: string; - readonly volumeId: string; - readonly fileName?: string; - }, + source: TranscriptSource, windowStartMs: number, ) { - const { provider, dir, volumeId, fileName } = source; + const { provider, format, dir, volumeId, fileName } = source; const exists = yield* fileSystem .exists(dir) .pipe(Effect.catchCause(() => Effect.succeed(false))); @@ -632,7 +646,7 @@ export const make = Effect.gen(function* () { const read = yield* Effect.forEach( files, (file) => - readFileRecords(file.path, file.size, file.mtimeMs, provider).pipe( + readFileRecords(file.path, file.size, file.mtimeMs, provider, format).pipe( Effect.map((result) => ({ path: file.path, ...result })), ), { concurrency: TRANSCRIPT_READ_CONCURRENCY }, @@ -818,10 +832,8 @@ export const make = Effect.gen(function* () { retentionCutoffMs: number, awaitRefresh: boolean, ) { - // The home resolvers ask for `Path` themselves; satisfy them from the - // instance we already hold so the scan stays context-free. const dirs = yield* resolveTranscriptDirs(settings, retentionCutoffMs).pipe( - Effect.provideService(Path.Path, path), + Effect.provideContext(readerContext), ); const home = NodeOS.homedir(); @@ -1100,7 +1112,7 @@ export const make = Effect.gen(function* () { } return retainedFiles; }); - const sharedSessions = sharedCodexSessions(filesByDir.flat()); + const crossFileSessions = sharedSessions(filesByDir.flat()); for (const [ index, @@ -1118,12 +1130,15 @@ export const make = Effect.gen(function* () { continue; } scannedFiles += 1; - const codexEventOccurrences = new Map(); + const eventOccurrences = new Map(); for (const record of file.records) { let usageRecord = record; - if (record.provider === "codex" && sharedSessions.has(record.sessionId)) { - // Match moved rollout copies without collapsing repeated equal events - // within one rollout (timestamps can have only second precision). + if ( + sharedSessionProviders.has(record.provider) && + crossFileSessions.has(record.sessionId) + ) { + // Match moved copies without collapsing repeated equal events + // within one file (timestamps can have only second precision). // Only sessions seen in several files can have a copy to match. const key = encodeUsageRecordKey([ record.provider, @@ -1132,8 +1147,8 @@ export const make = Effect.gen(function* () { record.model, record.totals, ]); - const occurrence = (codexEventOccurrences.get(key) ?? 0) + 1; - codexEventOccurrences.set(key, occurrence); + const occurrence = (eventOccurrences.get(key) ?? 0) + 1; + eventOccurrences.set(key, occurrence); usageRecord = { ...record, dedupeKey: key + ":" + occurrence }; } // Only sessions contributing in-window count; the mtime slack can diff --git a/apps/server/src/usage/antigravityUsageReader.ts b/apps/server/src/usage/antigravityUsageReader.ts index 8b1559cfedd7..9e91b6eacbb6 100644 --- a/apps/server/src/usage/antigravityUsageReader.ts +++ b/apps/server/src/usage/antigravityUsageReader.ts @@ -5,7 +5,7 @@ import * as NodePath from "node:path"; import * as NodeSqlite from "node:sqlite"; import * as NodeTimersPromises from "node:timers/promises"; -import type { UsageRecord } from "./usageTranscripts.ts"; +import type { UsageRecord } from "@t3tools/provider-core/server/usage"; type FieldValue = number | bigint | Uint8Array; type Fields = Map; diff --git a/apps/server/src/usage/cursorAccountCache.test.ts b/apps/server/src/usage/cursorAccountCache.test.ts index b6d20d36cf6a..da87e54c4157 100644 --- a/apps/server/src/usage/cursorAccountCache.test.ts +++ b/apps/server/src/usage/cursorAccountCache.test.ts @@ -1,7 +1,7 @@ import { assert, describe, it } from "@effect/vitest"; import { cursorFetchRange, mergeCursorFetch } from "./cursorAccountCache.ts"; -import type { UsageRecord } from "./usageTranscripts.ts"; +import type { UsageRecord } from "@t3tools/provider-core/server/usage"; const DAY_MS = 24 * 60 * 60 * 1000; diff --git a/apps/server/src/usage/cursorAccountCache.ts b/apps/server/src/usage/cursorAccountCache.ts index 4f3a127ec959..fd0a8ae6b08d 100644 --- a/apps/server/src/usage/cursorAccountCache.ts +++ b/apps/server/src/usage/cursorAccountCache.ts @@ -13,7 +13,7 @@ * @module cursorAccountCache */ import { cursorRateModel } from "./cursorUsageReader.ts"; -import type { UsageRecord } from "./usageTranscripts.ts"; +import type { UsageRecord } from "@t3tools/provider-core/server/usage"; export type CursorCredentialSource = string | { readonly kind: "keychain" }; diff --git a/apps/server/src/usage/cursorUsageReader.ts b/apps/server/src/usage/cursorUsageReader.ts index 95b87f1984bb..90d2f6c60cc8 100644 --- a/apps/server/src/usage/cursorUsageReader.ts +++ b/apps/server/src/usage/cursorUsageReader.ts @@ -8,7 +8,7 @@ import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; -import type { UsageRecord } from "./usageTranscripts.ts"; +import type { UsageRecord } from "@t3tools/provider-core/server/usage"; import { CursorKeychainTimeoutError, readMacCursorAccessToken, diff --git a/apps/server/src/usage/opencodeUsageReader.ts b/apps/server/src/usage/opencodeUsageReader.ts index d4414ac7b9a6..d6a835558ed6 100644 --- a/apps/server/src/usage/opencodeUsageReader.ts +++ b/apps/server/src/usage/opencodeUsageReader.ts @@ -5,7 +5,7 @@ import * as NodePath from "node:path"; import * as NodeSqlite from "node:sqlite"; import * as NodeTimersPromises from "node:timers/promises"; -import { totalTokens, type UsageRecord } from "./usageTranscripts.ts"; +import { totalTokens, type UsageRecord } from "@t3tools/provider-core/server/usage"; function object(value: unknown): Record { return typeof value === "object" && value !== null && !Array.isArray(value) diff --git a/apps/server/src/usage/usageAggregation.test.ts b/apps/server/src/usage/usageAggregation.test.ts index a27a9b101cc3..037928b2e7f2 100644 --- a/apps/server/src/usage/usageAggregation.test.ts +++ b/apps/server/src/usage/usageAggregation.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "@effect/vitest"; import { resolveModelAliases, UsageAggregator } from "./usageAggregation.ts"; import type { RateTable } from "./usagePricing.ts"; -import type { UsageRecord } from "./usageTranscripts.ts"; +import type { UsageRecord } from "@t3tools/provider-core/server/usage"; const rates: RateTable = new Map([ [ diff --git a/apps/server/src/usage/usageAggregation.ts b/apps/server/src/usage/usageAggregation.ts index f2923871a323..50ccaad34f3c 100644 --- a/apps/server/src/usage/usageAggregation.ts +++ b/apps/server/src/usage/usageAggregation.ts @@ -20,7 +20,7 @@ import type { UsageTokenTotals, } from "@t3tools/contracts"; -import { EMPTY_TOTALS, type UsageRecord } from "./usageTranscripts.ts"; +import { EMPTY_TOTALS, type UsageRecord } from "@t3tools/provider-core/server/usage"; import { cacheSavingsUsd, priceUsage, type RateTable } from "./usagePricing.ts"; /** diff --git a/apps/server/src/usage/usagePricing.test.ts b/apps/server/src/usage/usagePricing.test.ts index 0a324dd734b4..3afa9ff90824 100644 --- a/apps/server/src/usage/usagePricing.test.ts +++ b/apps/server/src/usage/usagePricing.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "@effect/vitest"; import { cursorRateModel } from "./cursorUsageReader.ts"; -import type { UsageSpeed } from "./usageTranscripts.ts"; +import type { UsageSpeed } from "@t3tools/provider-core/server/usage"; import { cacheSavingsUsd, createOverrideRateTable, diff --git a/apps/server/src/usage/usagePricing.ts b/apps/server/src/usage/usagePricing.ts index 0ec7ef8d622f..dda4e8257421 100644 --- a/apps/server/src/usage/usagePricing.ts +++ b/apps/server/src/usage/usagePricing.ts @@ -14,7 +14,7 @@ import type { UsageTokenTotals, } from "@t3tools/contracts"; -import type { UsageRecord, UsageSpeed } from "./usageTranscripts.ts"; +import type { UsageRecord, UsageSpeed } from "@t3tools/provider-core/server/usage"; /** Token rates for one billing speed. All values are USD per token. */ export interface TokenRates { diff --git a/apps/server/src/usage/usageScanCache.test.ts b/apps/server/src/usage/usageScanCache.test.ts index b59768cf892b..7d5637341b61 100644 --- a/apps/server/src/usage/usageScanCache.test.ts +++ b/apps/server/src/usage/usageScanCache.test.ts @@ -9,7 +9,9 @@ import { type CachedFile, type ScanCache, } from "./usageScanCache.ts"; -import type { UsageRecord } from "./usageTranscripts.ts"; +import type { UsageRecord } from "@t3tools/provider-core/server/usage"; + +import { TEST_FORMAT_MAP } from "./usageTestFormats.ts"; function record(overrides: Partial = {}): UsageRecord { return { @@ -36,7 +38,7 @@ function position(overrides: Partial = {}): CachedFile[" resumeOffset: 120, guardLength: 64, guardHash: 0xdeadbeef, - codexState: null, + state: null, ...overrides, }; } @@ -85,7 +87,7 @@ describe("scan cache round trip", () => { ], tailRecords: [], position: position({ - codexState: { + state: { model: "gpt-6-astra", speed: "ultrafast", sessionId: "session-c", @@ -97,7 +99,10 @@ describe("scan cache round trip", () => { }), }); - const restored = decodeScanCache(JSON.parse(JSON.stringify(encodeScanCache(original)))); + const restored = decodeScanCache( + JSON.parse(JSON.stringify(encodeScanCache(original))), + TEST_FORMAT_MAP, + ); expect(restored.size).toBe(4); expect(restored.get("/a.jsonl")).toEqual(original.get("/a.jsonl")); @@ -117,7 +122,9 @@ describe("scan cache round trip", () => { }, }; - expect(decodeScanCache(JSON.parse(JSON.stringify(poisoned))).has("/a.jsonl")).toBe(false); + expect( + decodeScanCache(JSON.parse(JSON.stringify(poisoned)), TEST_FORMAT_MAP).has("/a.jsonl"), + ).toBe(false); }); it("drops an entry whose guard length is outside the supported range", () => { @@ -129,7 +136,9 @@ describe("scan cache round trip", () => { files: { "/a.jsonl": { ...encoded.files["/a.jsonl"]!, gl: 1e20 } }, }; - expect(decodeScanCache(JSON.parse(JSON.stringify(poisoned))).has("/a.jsonl")).toBe(false); + expect( + decodeScanCache(JSON.parse(JSON.stringify(poisoned)), TEST_FORMAT_MAP).has("/a.jsonl"), + ).toBe(false); }); it("drops an entry whose speed is not a known index", () => { @@ -140,14 +149,16 @@ describe("scan cache round trip", () => { files: { "/a.jsonl": { ...encoded.files["/a.jsonl"]!, r: [[...row.slice(0, 10), true]] } }, }; - expect(decodeScanCache(JSON.parse(JSON.stringify(poisoned))).has("/a.jsonl")).toBe(false); + expect( + decodeScanCache(JSON.parse(JSON.stringify(poisoned)), TEST_FORMAT_MAP).has("/a.jsonl"), + ).toBe(false); }); it("rejects a document from before records carried a speed", () => { const encoded = encodeScanCache(cacheWith([["/a.jsonl", 100, [record()]]])); const previous = { ...encoded, version: 3 }; - expect(decodeScanCache(JSON.parse(JSON.stringify(previous))).size).toBe(0); + expect(decodeScanCache(JSON.parse(JSON.stringify(previous)), TEST_FORMAT_MAP).size).toBe(0); }); it("rewrites only changed entries and still restores the whole cache", () => { @@ -157,7 +168,7 @@ describe("scan cache round trip", () => { ["/b.jsonl", 200, [record({ sessionId: "session-b" })]], ]); const sources = { "claude\u0000/projects": { dir: "/projects", volumeId: "1:2" } }; - expect(decodeScanCache(JSON.parse(write(cache, { sources })))).toEqual(cache); + expect(decodeScanCache(JSON.parse(write(cache, { sources })), TEST_FORMAT_MAP)).toEqual(cache); // The replacement adds intern entries; /a's memoised indexes must hold. cache.set("/b.jsonl", { @@ -166,7 +177,7 @@ describe("scan cache round trip", () => { records: [record({ sessionId: "session-c", model: "claude-opus-5-5", dedupeKey: "msg_3:" })], }); const document = JSON.parse(write(cache, { sources })); - expect(decodeScanCache(document)).toEqual(cache); + expect(decodeScanCache(document, TEST_FORMAT_MAP)).toEqual(cache); expect(document.sources).toEqual(sources); }); @@ -181,9 +192,11 @@ describe("scan cache round trip", () => { it("treats a corrupt or foreign document as an empty cache", () => { // A bad cache should cost one cold scan, never a broken page. - expect(decodeScanCache(null).size).toBe(0); - expect(decodeScanCache("nonsense").size).toBe(0); - expect(decodeScanCache({ version: 999, models: [], sessions: [], files: {} }).size).toBe(0); + expect(decodeScanCache(null, TEST_FORMAT_MAP).size).toBe(0); + expect(decodeScanCache("nonsense", TEST_FORMAT_MAP).size).toBe(0); + expect( + decodeScanCache({ version: 999, models: [], sessions: [], files: {} }, TEST_FORMAT_MAP).size, + ).toBe(0); }); it("skips malformed file entries but keeps good ones", () => { @@ -193,7 +206,7 @@ describe("scan cache round trip", () => { files: { ...encoded.files, "/bad.jsonl": { s: "nope", m: 1, p: "claude", r: [] } }, }; - const restored = decodeScanCache(JSON.parse(JSON.stringify(withJunk))); + const restored = decodeScanCache(JSON.parse(JSON.stringify(withJunk)), TEST_FORMAT_MAP); expect([...restored.keys()]).toEqual(["/good.jsonl"]); }); @@ -203,7 +216,7 @@ describe("scan cache round trip", () => { const encoded = encodeScanCache(cacheWith([["/a.jsonl", 100, [record()]]])); const poisoned = { ...encoded, models: [1] }; - expect(decodeScanCache(JSON.parse(JSON.stringify(poisoned))).size).toBe(0); + expect(decodeScanCache(JSON.parse(JSON.stringify(poisoned)), TEST_FORMAT_MAP).size).toBe(0); }); it("drops the whole entry when any row is corrupt, forcing a cold re-parse", () => { @@ -223,7 +236,7 @@ describe("scan cache round trip", () => { }, }; - const restored = decodeScanCache(JSON.parse(JSON.stringify(poisoned))); + const restored = decodeScanCache(JSON.parse(JSON.stringify(poisoned)), TEST_FORMAT_MAP); expect(restored.has("/a.jsonl")).toBe(false); }); }); diff --git a/apps/server/src/usage/usageScanCache.ts b/apps/server/src/usage/usageScanCache.ts index 04e7ab4ce982..4673a4d8d072 100644 --- a/apps/server/src/usage/usageScanCache.ts +++ b/apps/server/src/usage/usageScanCache.ts @@ -15,9 +15,14 @@ * @module usageScanCache */ import type { UsageProviderKind } from "@t3tools/contracts"; +import type { + TranscriptUsageFormat, + UsageRecord, + UsageSpeed, +} from "@t3tools/provider-core/server/usage"; +import * as Schema from "effect/Schema"; import { GUARD_LENGTH, type TranscriptParsePosition } from "./usageTranscriptReader.ts"; -import type { CodexScanState, UsageRecord, UsageSpeed } from "./usageTranscripts.ts"; // v2: Codex fork-copy suppression changed what a file parses to, so v1 // entries would keep serving double-counted records forever. @@ -25,7 +30,7 @@ import type { CodexScanState, UsageRecord, UsageSpeed } from "./usageTranscripts // re-parses only its appended bytes instead of starting over. // v4: records carry Claude fast mode, which v3 rows never captured. // v5: Codex records carry their service tier. v4 rows store speed the same -// way, so v4 entries still load; see `decodeScanCache` for v4 Codex entries. +// way, so v4 entries still load; see `decodeScanCache` for v4 stateful entries. const USAGE_SCAN_CACHE_VERSION = 5 as const; const SPEED_COMPATIBLE_SINCE_VERSION = 4; @@ -41,10 +46,6 @@ export const LEGACY_SCAN_CACHE_FILE_NAME = "usage-scan-cache.json"; /** Serialised as the index into this list. */ const SPEEDS: readonly UsageSpeed[] = ["standard", "fast", "ultrafast"]; -function isSpeed(value: unknown): value is UsageSpeed { - return SPEEDS.some((speed) => speed === value); -} - export interface CachedFile { readonly size: number; readonly mtimeMs: number; @@ -92,8 +93,8 @@ interface SerializedFile { readonly o: number; readonly gl: number; readonly gh: number; - /** Codex reducer state at `o`; `null` for stateless providers. */ - readonly cs: CodexScanState | null; + /** The format's encoded reducer state at `o`; `null` for stateless formats. */ + readonly cs: unknown; } interface SerializedCache { @@ -147,7 +148,7 @@ function serializeFile(entry: CachedFile, tables: InternTables): SerializedFile o: entry.position.resumeOffset, gl: entry.position.guardLength, gh: entry.position.guardHash, - cs: entry.position.codexState, + cs: entry.position.state, }; } @@ -210,10 +211,15 @@ function isRecordArray(value: unknown): value is readonly unknown[] { * Rebuilds the cache from a parsed document. * * Anything malformed yields an empty cache rather than an error: a corrupt - * cache should cost one cold scan, never a broken page. + * cache should cost one cold scan, never a broken page. Entries of a provider + * without a transcript format in `formats` are dropped. */ -export function decodeScanCache(document: unknown): ScanCache { +export function decodeScanCache( + document: unknown, + formats: ReadonlyMap>, +): ScanCache { const cache: ScanCache = new Map(); + const isValidState = makeStateValidators(formats); if (typeof document !== "object" || document === null) return cache; const root = document as Partial; @@ -300,7 +306,9 @@ export function decodeScanCache(document: unknown): ScanCache { if (typeof raw !== "object" || raw === null) continue; const entry = raw as Partial; if (typeof entry.s !== "number" || typeof entry.m !== "number") continue; - if (entry.p !== "claude" && entry.p !== "codex" && entry.p !== "grok") continue; + if (entry.p === undefined) continue; + const format = formats.get(entry.p); + if (format === undefined) continue; if (!isRecordArray(entry.r) || !isRecordArray(entry.t)) continue; // Position fields feed byte offsets and a Buffer allocation in the reader, // so anything outside their real ranges must reject the entry: a bogus @@ -320,27 +328,34 @@ export function decodeScanCache(document: unknown): ScanCache { ) { continue; } - // v4 Codex records predate service tiers, so they all priced as standard. - // Keep them, because the rollout may be gone, but make a live rollout - // re-parse whole: no file has size -1, and a zero position cannot resume. - const legacyCodex = entry.p === "codex" && version < USAGE_SCAN_CACHE_VERSION; - const codexState = legacyCodex ? null : decodeCodexState(entry.cs); - if (codexState === undefined) continue; + // v4 records of stateful formats (Codex) predate service tiers, so they + // all priced as standard. Keep them, because the rollout may be gone, but + // make a live rollout re-parse whole: no file has size -1, and a zero + // position cannot resume. + const legacy = format.state !== undefined && version < USAGE_SCAN_CACHE_VERSION; + // A corrupt state disqualifies the entry: resuming with it would attach + // appended usage to the wrong model or replay fork-copied history. + if (!legacy && !isValidState.get(entry.p)?.(entry.cs)) continue; - const provider: UsageProviderKind = entry.p; + const provider = entry.p; const records = decodeRecords(entry.r, provider); const tailRecords = decodeRecords(entry.t, provider); if (records === null || tailRecords === null) continue; cache.set(path, { - size: legacyCodex ? -1 : entry.s, + size: legacy ? -1 : entry.s, mtimeMs: entry.m, provider, records, tailRecords, - position: legacyCodex - ? { resumeOffset: 0, guardLength: 0, guardHash: 0, codexState: null } - : { resumeOffset: entry.o, guardLength: entry.gl, guardHash: entry.gh, codexState }, + position: legacy + ? { resumeOffset: 0, guardLength: 0, guardHash: 0, state: null } + : { + resumeOffset: entry.o, + guardLength: entry.gl, + guardHash: entry.gh, + state: entry.cs, + }, }); } @@ -348,35 +363,21 @@ export function decodeScanCache(document: unknown): ScanCache { } /** - * Validates a persisted Codex reducer state. Returns `undefined` for a corrupt - * value, which disqualifies the entry: resuming with a bad state would attach - * appended usage to the wrong model or replay fork-copied history. + * Per provider, whether a persisted state is valid: `null`, or one the + * format's schema accepts. */ -function decodeCodexState(value: unknown): CodexScanState | null | undefined { - if (value === null) return null; - if (typeof value !== "object") return undefined; - const state = value as Partial; - if ( - typeof state.model !== "string" || - !isSpeed(state.speed) || - typeof state.sessionId !== "string" || - (state.lastUsageSignature !== null && typeof state.lastUsageSignature !== "string") || - typeof state.sawSessionMeta !== "boolean" || - typeof state.suppressingForkCopies !== "boolean" || - typeof state.forkCopyAnchorMs !== "number" || - !Number.isFinite(state.forkCopyAnchorMs) - ) { - return undefined; - } - return { - model: state.model, - speed: state.speed, - sessionId: state.sessionId, - lastUsageSignature: state.lastUsageSignature ?? null, - sawSessionMeta: state.sawSessionMeta, - suppressingForkCopies: state.suppressingForkCopies, - forkCopyAnchorMs: state.forkCopyAnchorMs, - }; +function makeStateValidators( + formats: ReadonlyMap>, +): ReadonlyMap boolean> { + return new Map( + [...formats].map(([provider, format]) => { + const isState = format.state === undefined ? undefined : Schema.is(format.state.schema); + return [ + provider, + (value: unknown) => value === null || (isState !== undefined && isState(value)), + ] as const; + }), + ); } /** Keeps saved usage after transcript cleanup, until the reporting retention expires. */ diff --git a/apps/server/src/usage/usageTestFormats.ts b/apps/server/src/usage/usageTestFormats.ts new file mode 100644 index 000000000000..be9a84a85b58 --- /dev/null +++ b/apps/server/src/usage/usageTestFormats.ts @@ -0,0 +1,21 @@ +/** + * The built-in transcript formats by provider, for engine and cache tests. + * + * @module usageTestFormats + */ +import type { UsageProviderKind } from "@t3tools/contracts"; +import type { TranscriptUsageFormat } from "@t3tools/provider-core/server/usage"; +import { grokUsageFormat } from "@t3tools/provider-grok/server/usage"; + +import { claudeUsageFormat } from "../provider/Drivers/claudeUsage.ts"; +import { codexUsageFormat } from "../provider/Drivers/codexUsage.ts"; + +export const TEST_FORMATS: Record<"claude" | "codex" | "grok", TranscriptUsageFormat> = { + claude: claudeUsageFormat, + codex: codexUsageFormat, + grok: grokUsageFormat, +}; + +export const TEST_FORMAT_MAP = new Map>( + Object.entries(TEST_FORMATS) as Array<[UsageProviderKind, TranscriptUsageFormat]>, +); diff --git a/apps/server/src/usage/usageTranscriptReader.test.ts b/apps/server/src/usage/usageTranscriptReader.test.ts index e6f19d50718a..183424cce3a7 100644 --- a/apps/server/src/usage/usageTranscriptReader.test.ts +++ b/apps/server/src/usage/usageTranscriptReader.test.ts @@ -8,6 +8,7 @@ import * as NodeSqlite from "node:sqlite"; import { afterEach, assert, beforeEach, describe, it } from "@effect/vitest"; +import { TEST_FORMATS } from "./usageTestFormats.ts"; import { readTranscriptRecords } from "./usageTranscriptReader.ts"; import { readOpenCodeUsage } from "./opencodeUsageReader.ts"; import { readCursorAccountUsage } from "./cursorUsageReader.ts"; @@ -99,20 +100,20 @@ describe("readTranscriptRecords resume", () => { it("parses only appended lines when resuming a grown file", async () => { const path = NodePath.join(dir, "claude.jsonl"); await NodeFSP.writeFile(path, claudeLine(1, 5) + claudeLine(2, 7)); - const first = await readTranscriptRecords(path, "claude"); + const first = await readTranscriptRecords(path, TEST_FORMATS.claude); assert.isNotNull(first); assert.strictEqual(first.records.length, 2); assert.isFalse(first.resumed); await NodeFSP.appendFile(path, claudeLine(3, 11)); - const second = await readTranscriptRecords(path, "claude", first.position); + const second = await readTranscriptRecords(path, TEST_FORMATS.claude, first.position); assert.isNotNull(second); assert.isTrue(second.resumed); assert.strictEqual(second.records.length, 1); assert.strictEqual(second.records[0]?.totals.outputTokens, 11); // The stitched result matches a from-scratch parse of the whole file. - const full = await readTranscriptRecords(path, "claude"); + const full = await readTranscriptRecords(path, TEST_FORMATS.claude); assert.isNotNull(full); assert.deepStrictEqual([...first.records, ...second.records], [...full.records]); }); @@ -123,7 +124,7 @@ describe("readTranscriptRecords resume", () => { path, codexMetaLine() + codexModelLine("gpt-5.2-codex") + codexTierLine("ultrafast"), ); - const first = await readTranscriptRecords(path, "codex"); + const first = await readTranscriptRecords(path, TEST_FORMATS.codex); assert.isNotNull(first); assert.strictEqual(first.records.length, 0); @@ -131,7 +132,7 @@ describe("readTranscriptRecords resume", () => { // session_meta of its own; model, tier, and session must come from the // state captured before the boundary. await NodeFSP.appendFile(path, codexUsageLine(9, 5)); - const second = await readTranscriptRecords(path, "codex", first.position); + const second = await readTranscriptRecords(path, TEST_FORMATS.codex, first.position); assert.isNotNull(second); assert.isTrue(second.resumed); assert.strictEqual(second.records.length, 1); @@ -146,14 +147,14 @@ describe("readTranscriptRecords resume", () => { path, codexMetaLine() + codexModelLine("gpt-5.2-codex") + codexUsageLine(9, 5), ); - const first = await readTranscriptRecords(path, "codex"); + const first = await readTranscriptRecords(path, TEST_FORMATS.codex); assert.isNotNull(first); assert.strictEqual(first.records.length, 1); // Codex re-emits an unchanged token_count on stream boundaries; the copy // lands after the resume point and must still be dropped. await NodeFSP.appendFile(path, codexUsageLine(9, 5) + codexUsageLine(21, 8)); - const second = await readTranscriptRecords(path, "codex", first.position); + const second = await readTranscriptRecords(path, TEST_FORMATS.codex, first.position); assert.isNotNull(second); assert.isTrue(second.resumed); assert.deepStrictEqual( @@ -166,7 +167,7 @@ describe("readTranscriptRecords resume", () => { const path = NodePath.join(dir, "claude.jsonl"); const unterminated = claudeLine(2, 7).trimEnd(); await NodeFSP.writeFile(path, claudeLine(1, 5) + unterminated); - const first = await readTranscriptRecords(path, "claude"); + const first = await readTranscriptRecords(path, TEST_FORMATS.claude); assert.isNotNull(first); assert.strictEqual(first.records.length, 1); assert.strictEqual(first.tailRecords.length, 1); @@ -175,7 +176,7 @@ describe("readTranscriptRecords resume", () => { // Completing the line and appending another re-reads from the resume // point, so the once-tail record arrives exactly once as a line record. await NodeFSP.appendFile(path, `\n${claudeLine(3, 11)}`); - const second = await readTranscriptRecords(path, "claude", first.position); + const second = await readTranscriptRecords(path, TEST_FORMATS.claude, first.position); assert.isNotNull(second); assert.isTrue(second.resumed); assert.deepStrictEqual( @@ -188,12 +189,12 @@ describe("readTranscriptRecords resume", () => { it("re-parses from the start when the guard bytes no longer match", async () => { const path = NodePath.join(dir, "claude.jsonl"); await NodeFSP.writeFile(path, claudeLine(1, 5)); - const first = await readTranscriptRecords(path, "claude"); + const first = await readTranscriptRecords(path, TEST_FORMATS.claude); assert.isNotNull(first); // Same path, larger size, different content: a replaced file, not growth. await NodeFSP.writeFile(path, claudeLine(4, 13) + claudeLine(5, 17)); - const second = await readTranscriptRecords(path, "claude", first.position); + const second = await readTranscriptRecords(path, TEST_FORMATS.claude, first.position); assert.isNotNull(second); assert.isFalse(second.resumed); assert.deepStrictEqual( @@ -205,11 +206,11 @@ describe("readTranscriptRecords resume", () => { it("re-parses from the start when the file shrank below the resume point", async () => { const path = NodePath.join(dir, "claude.jsonl"); await NodeFSP.writeFile(path, claudeLine(1, 5) + claudeLine(2, 7)); - const first = await readTranscriptRecords(path, "claude"); + const first = await readTranscriptRecords(path, TEST_FORMATS.claude); assert.isNotNull(first); await NodeFSP.writeFile(path, claudeLine(3, 11)); - const second = await readTranscriptRecords(path, "claude", first.position); + const second = await readTranscriptRecords(path, TEST_FORMATS.claude, first.position); assert.isNotNull(second); assert.isFalse(second.resumed); assert.deepStrictEqual( @@ -236,7 +237,7 @@ describe("readTranscriptRecords resume", () => { })}\n`; await NodeFSP.writeFile(path, bigLine + claudeLine(2, 7)); - const parsed = await readTranscriptRecords(path, "claude"); + const parsed = await readTranscriptRecords(path, TEST_FORMATS.claude); assert.isNotNull(parsed); assert.deepStrictEqual( parsed.records.map((record) => record.totals.outputTokens), @@ -245,7 +246,9 @@ describe("readTranscriptRecords resume", () => { }); it("returns null for an unreadable file", async () => { - assert.isNull(await readTranscriptRecords(NodePath.join(dir, "missing.jsonl"), "claude")); + assert.isNull( + await readTranscriptRecords(NodePath.join(dir, "missing.jsonl"), TEST_FORMATS.claude), + ); }); }); diff --git a/apps/server/src/usage/usageTranscriptReader.ts b/apps/server/src/usage/usageTranscriptReader.ts index bca22468a95e..16684e3a85ab 100644 --- a/apps/server/src/usage/usageTranscriptReader.ts +++ b/apps/server/src/usage/usageTranscriptReader.ts @@ -19,23 +19,16 @@ import * as NodeFSP from "node:fs/promises"; import * as NodePath from "node:path"; import * as NodeStringDecoder from "node:string_decoder"; -import type { UsageProviderKind } from "@t3tools/contracts"; +import type { + SelectedFields, + TranscriptUsageFormat, + UsageRecord, +} from "@t3tools/provider-core/server/usage"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; import { createTranscriptJsonReader } from "../project/AgentSessionJson.ts"; -import { - initialCodexScanState, - mightCarryUsage, - parseClaudeLine, - parseClaudeRecord, - parseCodexLine, - parseCodexRecord, - parseGrokLine, - parseGrokRecord, - type CodexScanState, - type UsageRecord, -} from "./usageTranscripts.ts"; - export interface TranscriptFile { readonly path: string; readonly size: number; @@ -60,8 +53,11 @@ export interface TranscriptParsePosition { readonly guardLength: number; /** FNV-1a hash of that window. */ readonly guardHash: number; - /** Codex reducer state as of `resumeOffset`; `null` for stateless providers. */ - readonly codexState: CodexScanState | null; + /** + * The format's reducer state as of `resumeOffset`, encoded by its schema; + * `null` for stateless formats. + */ + readonly state: unknown; } export interface TranscriptParseResult { @@ -89,45 +85,7 @@ const NEWLINE = 0x0a; const STAT_CONCURRENCY = 32; const CARRIAGE_RETURN = 0x0d; -type SelectedFields = { readonly [key: string]: true | SelectedFields }; - -// Keep the fields consumed by usageTranscripts, including reducer state and -// dedupe/cost metadata. A selected subtree (usage) keeps future token fields. -const USAGE_FIELDS: Record<"claude" | "codex" | "grok", SelectedFields> = { - claude: { - type: true, - timestamp: true, - requestId: true, - sessionId: true, - costUSD: true, - message: { id: true, model: true, usage: true }, - }, - codex: { - type: true, - timestamp: true, - payload: { - type: true, - id: true, - session_id: true, - model: true, - thread_settings: { service_tier: true }, - forked_from_id: true, - source: { subagent: { thread_spawn: { parent_thread_id: true } } }, - info: { last_token_usage: true }, - }, - }, - grok: { - timestamp: true, - params: { - sessionId: true, - _meta: { agentTimestampMs: true }, - update: { sessionUpdate: true, prompt_id: true, usage: true }, - }, - }, -}; - -function selectUsageFields(provider: UsageProviderKind) { - const fields = USAGE_FIELDS[provider === "codex" || provider === "grok" ? provider : "claude"]; +function selectUsageFields(fields: SelectedFields) { return (path: ReadonlyArray): boolean => { let selected: true | SelectedFields = fields; for (const key of path) { @@ -257,14 +215,12 @@ async function guardMatches( * still match, so only appended lines are read; otherwise the whole file is * re-parsed from the start and `resumed` reports `false`. * - * Codex carries the active model on `turn_context` lines and the service tier - * on `thread_settings_applied` lines. Neither holds usage of its own, but both - * still have to pass through the reducer to keep attribution and pricing - * correct. + * A stateful format resumes with the state saved at that position; a saved + * state its schema rejects re-parses the file whole. */ -export async function readTranscriptRecords( +export async function readTranscriptRecords( filePath: string, - provider: UsageProviderKind, + format: TranscriptUsageFormat, resumeFrom?: TranscriptParsePosition, options?: { readonly streamingThresholdBytes?: number }, ): Promise { @@ -277,41 +233,31 @@ export async function readTranscriptRecords( } try { - let codexState = initialCodexScanState(); + const codec = format.state; + const decodeState = codec === undefined ? undefined : Schema.decodeUnknownOption(codec.schema); + const encodeState = codec === undefined ? undefined : Schema.encodeSync(codec.schema); + // Stateless formats never read their state argument. + let state = codec === undefined ? (undefined as State) : codec.initial(); let resumed = false; let start = 0; + const savedState = + resumeFrom === undefined || decodeState === undefined || resumeFrom.state === null + ? Option.none() + : decodeState(resumeFrom.state); if ( resumeFrom !== undefined && resumeFrom.resumeOffset > 0 && - (provider !== "codex" || resumeFrom.codexState !== null) && + (codec === undefined || Option.isSome(savedState)) && (await guardMatches(handle, resumeFrom)) ) { - if (resumeFrom.codexState !== null) codexState = { ...resumeFrom.codexState }; + if (Option.isSome(savedState)) state = savedState.value; start = resumeFrom.resumeOffset; resumed = true; } - const parseLine = (line: string, state: CodexScanState, out: UsageRecord[]): void => { - if (provider === "codex") { - if ( - !mightCarryUsage(line, provider) && - !line.includes('"turn_context"') && - !line.includes('"thread_settings_applied"') && - !line.includes('"session_meta"') - ) { - return; - } - const record = parseCodexLine(line, state); - if (record !== null) out.push(record); - return; - } - if (!mightCarryUsage(line, provider)) return; - if (provider === "grok") { - for (const grokRecord of parseGrokLine(line)) out.push(grokRecord); - return; - } - const record = parseClaudeLine(line); - if (record !== null) out.push(record); + const parseLine = (line: string, lineState: State, out: UsageRecord[]): void => { + if (!format.mightCarryUsage(line)) return; + for (const record of format.parseLine(line, lineState)) out.push(record); }; const toLineString = (lineBuffer: Buffer): string => { @@ -331,7 +277,7 @@ export async function readTranscriptRecords( let pendingBytes = 0; let streaming: ReturnType | undefined; let decoder: NodeStringDecoder.StringDecoder | undefined; - const selectPath = selectUsageFields(provider); + const selectPath = selectUsageFields(format.selectFields); const append = (segment: Buffer) => { if (!streaming && pendingBytes + segment.length <= streamingThresholdBytes) { @@ -350,25 +296,17 @@ export async function readTranscriptRecords( } streaming.write(decoder!.write(segment)); }; - const finish = (state: CodexScanState, out: UsageRecord[]) => { + const finish = (lineState: State, out: UsageRecord[]) => { if (streaming) { streaming.write(decoder!.end()); const projected = streaming.finish(); - if (provider === "grok") { - out.push(...parseGrokRecord(projected)); - } else { - const record = - provider === "codex" - ? parseCodexRecord(projected, state) - : parseClaudeRecord(projected); - if (record !== null) out.push(record); - } + for (const record of format.parseProjected(projected, lineState)) out.push(record); } else if (pendingBytes > 0) { const line = pendingChunks.length === 1 ? pendingChunks[0]! : Buffer.concat(pendingChunks, pendingBytes); - parseLine(toLineString(line), state, out); + parseLine(toLineString(line), lineState, out); } pendingChunks = []; pendingBytes = 0; @@ -391,10 +329,10 @@ export async function readTranscriptRecords( // Most lines fit in the current chunk. Avoid buffering/streaming // machinery on this hot path. if (!streaming && pendingBytes === 0) { - parseLine(toLineString(chunk.subarray(lineStart, newlineIndex)), codexState, records); + parseLine(toLineString(chunk.subarray(lineStart, newlineIndex)), state, records); } else { append(chunk.subarray(lineStart, newlineIndex)); - finish(codexState, records); + finish(state, records); } lineStart = newlineIndex + 1; resumeOffset = scanOffset + lineStart; @@ -402,8 +340,14 @@ export async function readTranscriptRecords( scanOffset += chunk.length; } + // The unfinished tail parses against a copy, so the saved state stays at + // `resumeOffset` and the next scan replays the tail from it. + const encodedState = encodeState === undefined ? null : encodeState(state); const tailRecords: UsageRecord[] = []; - finish({ ...codexState }, tailRecords); + finish( + decodeState === undefined ? state : Option.getOrThrow(decodeState(encodedState)), + tailRecords, + ); const guardLength = Math.min(GUARD_LENGTH, resumeOffset); let guardHash = 0; @@ -420,7 +364,7 @@ export async function readTranscriptRecords( resumeOffset, guardLength, guardHash, - codexState: provider === "codex" ? codexState : null, + state: encodedState, }, resumed, }; diff --git a/apps/server/src/usage/usageTranscriptStreaming.test.ts b/apps/server/src/usage/usageTranscriptStreaming.test.ts index fd23593e5523..0467c17c5d49 100644 --- a/apps/server/src/usage/usageTranscriptStreaming.test.ts +++ b/apps/server/src/usage/usageTranscriptStreaming.test.ts @@ -5,6 +5,7 @@ import * as NodePath from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "@effect/vitest"; +import { TEST_FORMATS } from "./usageTestFormats.ts"; import { readTranscriptRecords as readWithDefaultThreshold, type TranscriptParsePosition, @@ -16,7 +17,10 @@ const readTranscriptRecords = ( path: string, provider: "claude" | "codex" | "grok", position?: TranscriptParsePosition, -) => readWithDefaultThreshold(path, provider, position, { streamingThresholdBytes: 256 * 1024 }); +) => + readWithDefaultThreshold(path, TEST_FORMATS[provider], position, { + streamingThresholdBytes: 256 * 1024, + }); let dir: string; beforeEach(async () => { @@ -144,7 +148,7 @@ describe("large usage records", () => { ); const actual = await scan(large, provider); expect(actual.records).toEqual(expected.records); - expect(actual.position.codexState).toEqual(expected.position.codexState); + expect(actual.position.state).toEqual(expected.position.state); } }, ); diff --git a/apps/server/src/usage/usageTranscripts.test.ts b/apps/server/src/usage/usageTranscripts.test.ts deleted file mode 100644 index 8d668a139822..000000000000 --- a/apps/server/src/usage/usageTranscripts.test.ts +++ /dev/null @@ -1,598 +0,0 @@ -import { describe, expect, it } from "@effect/vitest"; - -import { - GROK_COST_USD_TICKS_PER_DOLLAR, - initialCodexScanState, - parseClaudeLine, - parseCodexLine, - parseGrokLine, - totalTokens, -} from "./usageTranscripts.ts"; - -/** Shaped after a real Claude Code assistant record. */ -function claudeLine(overrides: { - messageId: string; - contentType: string; - model?: string; - outputTokens?: number; - speed?: string; -}): string { - return JSON.stringify({ - type: "assistant", - timestamp: "2026-08-07T04:05:13.944Z", - sessionId: "5a128faa-8253-489e-b935-6c08e8e670c0", - cwd: "/home/theo/project", - message: { - id: overrides.messageId, - role: "assistant", - model: overrides.model ?? "claude-fable-5", - content: [{ type: overrides.contentType }], - usage: { - input_tokens: 2, - cache_creation_input_tokens: 66818, - cache_read_input_tokens: 1000, - output_tokens: overrides.outputTokens ?? 286, - ...(overrides.speed === undefined ? {} : { speed: overrides.speed }), - }, - }, - }); -} - -describe("parseClaudeLine", () => { - it("extracts token totals and a dedupe key", () => { - const record = parseClaudeLine(claudeLine({ messageId: "msg_1", contentType: "text" })); - - expect(record).not.toBeNull(); - expect(record?.provider).toBe("claude"); - expect(record?.model).toBe("claude-fable-5"); - expect(record?.totals).toEqual({ - uncachedInputTokens: 2, - cachedInputTokens: 1000, - cacheCreationTokens: 66818, - outputTokens: 286, - reasoningTokens: 0, - }); - expect(record?.dedupeKey).toBe("msg_1:"); - expect(record?.speed).toBe("standard"); - }); - - it("marks fast-mode requests", () => { - const line = (speed: string) => - parseClaudeLine(claudeLine({ messageId: "msg_1", contentType: "text", speed })); - - expect(line("fast")?.speed).toBe("fast"); - expect(line("standard")?.speed).toBe("standard"); - }); - - it("gives every content block of one message the same dedupe key", () => { - // T3 Code writes one record per content block, each repeating the parent - // message's full usage. Summing them would overcount ~2.4x on real data. - const text = parseClaudeLine(claudeLine({ messageId: "msg_2", contentType: "text" })); - const toolUse = parseClaudeLine(claudeLine({ messageId: "msg_2", contentType: "tool_use" })); - - expect(text?.dedupeKey).toBe(toolUse?.dedupeKey); - expect(text?.totals).toEqual(toolUse?.totals); - }); - - it("ignores records that are not assistant messages", () => { - expect(parseClaudeLine(JSON.stringify({ type: "user", message: {} }))).toBeNull(); - expect(parseClaudeLine("not json")).toBeNull(); - }); -}); - -describe("parseCodexLine", () => { - const sessionMeta = JSON.stringify({ - type: "session_meta", - timestamp: "2026-08-01T05:17:41.289Z", - payload: { type: "session_meta", id: "019fbbc1-b12c-7360-a685-28c181f0025f" }, - }); - const turnContext = JSON.stringify({ - type: "turn_context", - timestamp: "2026-08-01T05:17:42.694Z", - payload: { type: "turn_context", model: "gpt-5.6-sol" }, - }); - const tokenCount = (inputTokens: number, cached: number, output: number, reasoning: number) => - JSON.stringify({ - type: "event_msg", - timestamp: "2026-08-01T05:17:49.919Z", - payload: { - type: "token_count", - info: { - last_token_usage: { - input_tokens: inputTokens, - cached_input_tokens: cached, - cache_write_input_tokens: 0, - output_tokens: output, - reasoning_output_tokens: reasoning, - }, - }, - }, - }); - - it("attributes usage to the model from the preceding turn context", () => { - const state = initialCodexScanState(); - parseCodexLine(sessionMeta, state); - parseCodexLine(turnContext, state); - const record = parseCodexLine(tokenCount(19239, 11008, 299, 116), state); - - expect(record?.provider).toBe("codex"); - expect(record?.model).toBe("gpt-5.6-sol"); - expect(record?.sessionId).toBe("019fbbc1-b12c-7360-a685-28c181f0025f"); - // Codex reports input_tokens inclusive of the cached portion. - expect(record?.totals.uncachedInputTokens).toBe(19239 - 11008); - expect(record?.totals.cachedInputTokens).toBe(11008); - expect(record?.totals.reasoningTokens).toBe(116); - }); - - it("skips a repeated token_count so deltas are not double counted", () => { - const state = initialCodexScanState(); - parseCodexLine(turnContext, state); - const first = parseCodexLine(tokenCount(100, 0, 10, 0), state); - const repeat = parseCodexLine(tokenCount(100, 0, 10, 0), state); - - expect(first).not.toBeNull(); - expect(repeat).toBeNull(); - }); - - it("drops usage that arrives before any model is known", () => { - const state = initialCodexScanState(); - expect(parseCodexLine(tokenCount(100, 0, 10, 0), state)).toBeNull(); - }); - - it("does not let a pre-model event poison the duplicate signature", () => { - // A token_count before its turn_context is dropped; the identical event - // re-emitted once the model is known must still be counted. - const state = initialCodexScanState(); - expect(parseCodexLine(tokenCount(100, 0, 10, 0), state)).toBeNull(); - parseCodexLine(turnContext, state); - expect(parseCodexLine(tokenCount(100, 0, 10, 0), state)).not.toBeNull(); - }); - - it("carries the service tier from the latest thread settings", () => { - const settings = (thread_settings: Record) => - JSON.stringify({ - type: "event_msg", - timestamp: "2026-08-01T05:17:42.000Z", - payload: { type: "thread_settings_applied", thread_settings }, - }); - const state = initialCodexScanState(); - parseCodexLine(turnContext, state); - const speedAfter = (line: string, output: number) => { - parseCodexLine(line, state); - return parseCodexLine(tokenCount(100, 0, output, 0), state)?.speed; - }; - - expect(parseCodexLine(tokenCount(100, 0, 1, 0), state)?.speed).toBe("standard"); - expect(speedAfter(settings({ service_tier: "ultrafast" }), 2)).toBe("ultrafast"); - expect(speedAfter(settings({ service_tier: "priority" }), 3)).toBe("fast"); - // Codex omits the field when no tier was requested. - expect(speedAfter(settings({ model: "gpt-6-astra" }), 4)).toBe("standard"); - }); - - // A forked/subagent rollout opens with the parent's history copied in and - // every line re-stamped to the fork instant, then the ancestors' session - // metas. Counting those again multiplied usage ~1.85x on real data (#5758). - describe("forked rollouts", () => { - const meta = (overrides: { - id: string; - timestamp: string; - forkedFromId?: string; - spawnParentId?: string; - }) => - JSON.stringify({ - type: "session_meta", - timestamp: overrides.timestamp, - payload: { - type: "session_meta", - id: overrides.id, - ...(overrides.forkedFromId === undefined - ? {} - : { forked_from_id: overrides.forkedFromId }), - ...(overrides.spawnParentId === undefined - ? {} - : { - source: { - subagent: { thread_spawn: { parent_thread_id: overrides.spawnParentId } }, - }, - }), - }, - }); - const stamped = (timestamp: string, line: string) => { - const parsed = JSON.parse(line) as { timestamp: string }; - parsed.timestamp = timestamp; - return JSON.stringify(parsed); - }; - - it("keeps the child session id over copied ancestor metas", () => { - const state = initialCodexScanState(); - parseCodexLine(meta({ id: "child", timestamp: "2026-08-01T05:00:00.000Z" }), state); - parseCodexLine(meta({ id: "parent", timestamp: "2026-08-01T05:00:00.000Z" }), state); - parseCodexLine(turnContext, state); - const record = parseCodexLine(tokenCount(100, 0, 10, 0), state); - - expect(record?.sessionId).toBe("child"); - }); - - it("drops the re-stamped copied burst and keeps the first real event", () => { - const state = initialCodexScanState(); - const forkInstant = "2026-08-01T05:00:00.000Z"; - parseCodexLine(meta({ id: "child", timestamp: forkInstant, forkedFromId: "parent" }), state); - parseCodexLine(meta({ id: "parent", timestamp: forkInstant }), state); - parseCodexLine(stamped(forkInstant, turnContext), state); - - // Copied history: written in one burst at the fork instant. - expect( - parseCodexLine(stamped("2026-08-01T05:00:00.001Z", tokenCount(100, 0, 10, 0)), state), - ).toBeNull(); - expect( - parseCodexLine(stamped("2026-08-01T05:00:00.002Z", tokenCount(200, 0, 20, 0)), state), - ).toBeNull(); - - // The child's first genuine turn lands seconds later and must count. - const real = parseCodexLine( - stamped("2026-08-01T05:00:06.000Z", tokenCount(300, 0, 30, 0)), - state, - ); - expect(real).not.toBeNull(); - expect(real?.totals.outputTokens).toBe(30); - - // Suppression never restarts, even for closely spaced later events. - const next = parseCodexLine( - stamped("2026-08-01T05:00:06.100Z", tokenCount(400, 0, 40, 0)), - state, - ); - expect(next).not.toBeNull(); - }); - - it("recognizes subagent spawns without forked_from_id", () => { - const state = initialCodexScanState(); - const spawnInstant = "2026-08-01T05:00:00.000Z"; - parseCodexLine( - meta({ id: "child", timestamp: spawnInstant, spawnParentId: "parent" }), - state, - ); - parseCodexLine(stamped(spawnInstant, turnContext), state); - expect( - parseCodexLine(stamped("2026-08-01T05:00:00.001Z", tokenCount(100, 0, 10, 0)), state), - ).toBeNull(); - }); - - it("does not suppress anything in a rollout that is not a fork", () => { - const state = initialCodexScanState(); - parseCodexLine(meta({ id: "root", timestamp: "2026-08-01T05:00:00.000Z" }), state); - parseCodexLine(stamped("2026-08-01T05:00:00.100Z", turnContext), state); - const record = parseCodexLine( - stamped("2026-08-01T05:00:00.200Z", tokenCount(100, 0, 10, 0)), - state, - ); - expect(record).not.toBeNull(); - }); - }); -}); - -describe("totalTokens", () => { - it("does not add reasoning on top of output", () => { - expect( - totalTokens({ - uncachedInputTokens: 10, - cachedInputTokens: 20, - cacheCreationTokens: 30, - outputTokens: 40, - reasoningTokens: 25, - }), - ).toBe(100); - }); -}); - -describe("parseGrokLine", () => { - /** Shaped after a real Grok Build `turn_completed` session update. */ - function turnCompleted(overrides?: { - sessionId?: string; - promptId?: string; - timestamp?: number; - agentTimestampMs?: number; - usage?: Record; - modelUsage?: Record> | null; - }): string { - const modelUsage = - overrides && "modelUsage" in overrides - ? overrides.modelUsage - : { - "grok-4.5-build": { - inputTokens: 20_272, - outputTokens: 272, - totalTokens: 20_544, - cachedReadTokens: 11_264, - cacheCreationTokens: 0, - reasoningTokens: 180, - costUsdTicks: 230_272_000, - }, - }; - - return JSON.stringify({ - timestamp: overrides?.timestamp ?? 1_786_372_566, - method: "_x.ai/session/update", - params: { - sessionId: overrides?.sessionId ?? "019fec1a-12f7-72f2-9b1f-7778a00aea3c", - update: { - sessionUpdate: "turn_completed", - prompt_id: overrides?.promptId ?? "prompt-1", - stop_reason: "end_turn", - usage: { - inputTokens: 20_272, - outputTokens: 272, - totalTokens: 20_544, - cachedReadTokens: 11_264, - cacheCreationTokens: 0, - reasoningTokens: 180, - costUsdTicks: 230_272_000, - ...(modelUsage === null ? {} : { modelUsage }), - ...overrides?.usage, - }, - }, - _meta: { - eventId: "event-1", - agentTimestampMs: overrides?.agentTimestampMs ?? 1_786_372_566_485, - }, - }, - }); - } - - it("extracts per-model totals and provider-reported cost ticks", () => { - const records = parseGrokLine(turnCompleted()); - - expect(records).toHaveLength(1); - const [record] = records; - expect(record?.provider).toBe("grok"); - expect(record?.model).toBe("grok-4.5-build"); - expect(record?.sessionId).toBe("019fec1a-12f7-72f2-9b1f-7778a00aea3c"); - expect(record?.timestampMs).toBe(1_786_372_566_485); - expect(record?.totals).toEqual({ - uncachedInputTokens: 20_272 - 11_264, - cachedInputTokens: 11_264, - cacheCreationTokens: 0, - outputTokens: 272, - reasoningTokens: 180, - }); - expect(record?.reportedCostUsd).toBeCloseTo(230_272_000 / GROK_COST_USD_TICKS_PER_DOLLAR, 12); - expect(record?.dedupeKey).toBe("019fec1a-12f7-72f2-9b1f-7778a00aea3c:prompt-1:grok-4.5-build"); - }); - - it("emits one record per model when modelUsage has several entries", () => { - const records = parseGrokLine( - turnCompleted({ - modelUsage: { - "grok-4.5": { - inputTokens: 1000, - outputTokens: 50, - cachedReadTokens: 400, - reasoningTokens: 20, - costUsdTicks: 50_000_000, - }, - "grok-composer-2.5-fast": { - inputTokens: 200, - outputTokens: 30, - cachedReadTokens: 100, - reasoningTokens: 0, - costUsdTicks: 10_000_000, - }, - }, - }), - ); - - expect(records.map((record) => record.model).toSorted()).toEqual([ - "grok-4.5", - "grok-composer-2.5-fast", - ]); - expect(records.every((record) => record.provider === "grok")).toBe(true); - expect(records.find((record) => record.model === "grok-4.5")?.reportedCostUsd).toBeCloseTo( - 0.005, - 12, - ); - }); - - it("inherits top-level cost ticks for a single model without its own ticks", () => { - const records = parseGrokLine( - turnCompleted({ - modelUsage: { - "grok-4.5-build": { - inputTokens: 1000, - outputTokens: 10, - cachedReadTokens: 0, - reasoningTokens: 0, - }, - }, - usage: { costUsdTicks: GROK_COST_USD_TICKS_PER_DOLLAR }, - }), - ); - - expect(records).toHaveLength(1); - expect(records[0]?.reportedCostUsd).toBe(1); - }); - - it("falls back to a generic grok model when modelUsage is absent", () => { - const records = parseGrokLine(turnCompleted({ modelUsage: null })); - - expect(records).toHaveLength(1); - const [record] = records; - expect(record?.provider).toBe("grok"); - expect(record?.model).toBe("grok"); - expect(record?.totals).toEqual({ - uncachedInputTokens: 20_272 - 11_264, - cachedInputTokens: 11_264, - cacheCreationTokens: 0, - outputTokens: 272, - reasoningTokens: 180, - }); - expect(record?.reportedCostUsd).toBeCloseTo(230_272_000 / GROK_COST_USD_TICKS_PER_DOLLAR, 12); - expect(record?.dedupeKey).toBe("019fec1a-12f7-72f2-9b1f-7778a00aea3c:prompt-1:grok"); - }); - - it("pro-rates top-level cost ticks across multi-model turns without per-model ticks", () => { - const records = parseGrokLine( - turnCompleted({ - modelUsage: { - "grok-4.5": { - inputTokens: 300, - outputTokens: 0, - cachedReadTokens: 0, - reasoningTokens: 0, - }, - "grok-composer-2.5-fast": { - inputTokens: 100, - outputTokens: 0, - cachedReadTokens: 0, - reasoningTokens: 0, - }, - }, - usage: { costUsdTicks: GROK_COST_USD_TICKS_PER_DOLLAR }, - }), - ); - - expect(records).toHaveLength(2); - const byModel = Object.fromEntries(records.map((record) => [record.model, record])); - expect(byModel["grok-4.5"]?.reportedCostUsd).toBeCloseTo(0.75, 12); - expect(byModel["grok-composer-2.5-fast"]?.reportedCostUsd).toBeCloseTo(0.25, 12); - const sum = - (byModel["grok-4.5"]?.reportedCostUsd ?? 0) + - (byModel["grok-composer-2.5-fast"]?.reportedCostUsd ?? 0); - expect(sum).toBeCloseTo(1, 12); - }); - - it("pro-rates aggregate cost when a zero-token sibling carries costUsdTicks: 0", () => { - const records = parseGrokLine( - turnCompleted({ - modelUsage: { - "grok-4.5": { - inputTokens: 300, - outputTokens: 0, - cachedReadTokens: 0, - reasoningTokens: 0, - }, - "grok-composer-2.5-fast": { - inputTokens: 100, - outputTokens: 0, - cachedReadTokens: 0, - reasoningTokens: 0, - }, - "empty-sibling": { - inputTokens: 0, - outputTokens: 0, - cachedReadTokens: 0, - reasoningTokens: 0, - costUsdTicks: 0, - }, - }, - usage: { costUsdTicks: GROK_COST_USD_TICKS_PER_DOLLAR }, - }), - ); - - expect(records).toHaveLength(2); - expect(records.every((record) => record.model !== "empty-sibling")).toBe(true); - const byModel = Object.fromEntries(records.map((record) => [record.model, record])); - expect(byModel["grok-4.5"]?.reportedCostUsd).toBeCloseTo(0.75, 12); - expect(byModel["grok-composer-2.5-fast"]?.reportedCostUsd).toBeCloseTo(0.25, 12); - const sum = - (byModel["grok-4.5"]?.reportedCostUsd ?? 0) + - (byModel["grok-composer-2.5-fast"]?.reportedCostUsd ?? 0); - expect(sum).toBeCloseTo(1, 12); - }); - - it("allocates leftover aggregate ticks to models that omit per-model ticks", () => { - const records = parseGrokLine( - turnCompleted({ - modelUsage: { - "grok-4.5": { - inputTokens: 300, - outputTokens: 0, - cachedReadTokens: 0, - reasoningTokens: 0, - costUsdTicks: 0.4 * GROK_COST_USD_TICKS_PER_DOLLAR, - }, - "grok-composer-2.5-fast": { - inputTokens: 100, - outputTokens: 0, - cachedReadTokens: 0, - reasoningTokens: 0, - }, - }, - usage: { costUsdTicks: GROK_COST_USD_TICKS_PER_DOLLAR }, - }), - ); - - expect(records).toHaveLength(2); - const byModel = Object.fromEntries(records.map((record) => [record.model, record])); - expect(byModel["grok-4.5"]?.reportedCostUsd).toBeCloseTo(0.4, 12); - expect(byModel["grok-composer-2.5-fast"]?.reportedCostUsd).toBeCloseTo(0.6, 12); - const sum = - (byModel["grok-4.5"]?.reportedCostUsd ?? 0) + - (byModel["grok-composer-2.5-fast"]?.reportedCostUsd ?? 0); - expect(sum).toBeCloseTo(1, 12); - }); - - it("does not invent a colliding dedupe key when prompt_id is missing", () => { - const line = JSON.stringify({ - timestamp: 1_786_372_566, - method: "_x.ai/session/update", - params: { - sessionId: "s1", - update: { - sessionUpdate: "turn_completed", - usage: { - inputTokens: 10, - outputTokens: 2, - modelUsage: { - "grok-4.5": { inputTokens: 10, outputTokens: 2 }, - }, - }, - }, - }, - }); - - expect(parseGrokLine(line)[0]?.dedupeKey).toBeNull(); - }); - - it("ignores non-turn lines and empty usage", () => { - expect(parseGrokLine(JSON.stringify({ method: "session/update", params: {} }))).toEqual([]); - expect(parseGrokLine("not json")).toEqual([]); - expect( - parseGrokLine( - turnCompleted({ - modelUsage: { - "grok-4.5-build": { - inputTokens: 0, - outputTokens: 0, - cachedReadTokens: 0, - reasoningTokens: 0, - costUsdTicks: 0, - }, - }, - }), - ), - ).toEqual([]); - }); - - it("falls back to the outer unix-seconds timestamp when agent meta is missing", () => { - const line = JSON.stringify({ - timestamp: 1_786_372_566, - method: "_x.ai/session/update", - params: { - sessionId: "s1", - update: { - sessionUpdate: "turn_completed", - prompt_id: "p1", - usage: { - inputTokens: 10, - outputTokens: 2, - modelUsage: { - "grok-4.5": { inputTokens: 10, outputTokens: 2 }, - }, - }, - }, - }, - }); - - const records = parseGrokLine(line); - expect(records[0]?.timestampMs).toBe(1_786_372_566_000); - }); -}); diff --git a/apps/server/src/usage/usageTranscripts.ts b/apps/server/src/usage/usageTranscripts.ts deleted file mode 100644 index 451f8a726b3e..000000000000 --- a/apps/server/src/usage/usageTranscripts.ts +++ /dev/null @@ -1,530 +0,0 @@ -/** - * Pure parsers for the provider CLIs' on-disk session transcripts. - * - * Each parser is a line-at-a-time reducer so callers can stream large files - * without materialising them. None of them touch the filesystem. - * - * @module usageTranscripts - */ -import type { UsageProviderKind, UsageTokenTotals } from "@t3tools/contracts"; - -/** - * Billing speed of a request. Faster speeds bill at a model-specific premium. - * Claude fast mode and Codex `priority` are `fast`; Codex `ultrafast` is its - * own, more expensive tier. - */ -export type UsageSpeed = "standard" | "fast" | "ultrafast"; - -export interface UsageRecord { - readonly provider: UsageProviderKind; - readonly timestampMs: number; - readonly model: string; - /** - * Rate-table key when the provider's display name carries tiers the table - * does not know, such as Cursor's `claude-opus-5-5-high`. Defaults to `model`. - */ - readonly rateModel?: string; - readonly sessionId: string; - readonly totals: UsageTokenTotals; - readonly reportedCostUsd: number | null; - /** Only Claude Code and Codex record a speed; other providers are `standard`. */ - readonly speed: UsageSpeed; - /** - * Key for cross-file de-duplication, or `null` when the record is inherently - * unique and needs no dedup. - */ - readonly dedupeKey: string | null; -} - -const EMPTY_TOTALS: UsageTokenTotals = { - uncachedInputTokens: 0, - cachedInputTokens: 0, - cacheCreationTokens: 0, - outputTokens: 0, - reasoningTokens: 0, -}; - -function int(value: unknown): number { - return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.trunc(value) : 0; -} - -function parseTimestampMs(value: unknown): number | null { - if (typeof value !== "string") return null; - const parsed = Date.parse(value); - return Number.isNaN(parsed) ? null : parsed; -} - -export function totalTokens(totals: UsageTokenTotals): number { - // reasoningTokens is a subset of outputTokens and must not be added again. - return ( - totals.uncachedInputTokens + - totals.cachedInputTokens + - totals.cacheCreationTokens + - totals.outputTokens - ); -} - -/** - * Cheap substring gate applied before `JSON.parse`. - * - * Transcripts are mostly tool output; only a minority of lines carry usage. On - * a 30-day window this skips roughly half the lines outright and is worth about - * an order of magnitude. - */ -export function mightCarryUsage(line: string, provider: UsageProviderKind): boolean { - if (provider === "claude") return line.includes('"usage"'); - if (provider === "grok") return line.includes('"turn_completed"'); - return line.includes('"token_count"'); -} - -/** - * Grok reports cost in integer ticks where `1 USD = 10^10` ticks. See Grok - * headless `total_cost_usd_ticks`. Convert to dollars for pricing. - */ -export const GROK_COST_USD_TICKS_PER_DOLLAR = 10_000_000_000; - -function grokCostTicksToUsd(ticks: unknown): number | null { - if (typeof ticks !== "number" || !Number.isFinite(ticks) || ticks < 0) return null; - return ticks / GROK_COST_USD_TICKS_PER_DOLLAR; -} - -/* -------------------------------------------------------------------------- */ -/* Claude Code */ -/* -------------------------------------------------------------------------- */ - -/** - * Parses one line of a Claude Code transcript. - * - * T3 Code writes one record per assistant *content block*, and every one of - * those records repeats the same complete `usage` object for the parent - * message. Summing them overcounts by roughly 2.4x on a real workload, so the - * caller must drop repeats by `dedupeKey` and keep the first. - */ -export function parseClaudeLine(line: string): UsageRecord | null { - let parsed: unknown; - try { - parsed = JSON.parse(line); - } catch { - return null; - } - return parseClaudeRecord(parsed); -} - -export function parseClaudeRecord(parsed: unknown): UsageRecord | null { - if (typeof parsed !== "object" || parsed === null) return null; - - const record = parsed as Record; - if (record["type"] !== "assistant") return null; - - const message = record["message"]; - if (typeof message !== "object" || message === null) return null; - const messageRecord = message as Record; - - const usage = messageRecord["usage"]; - if (typeof usage !== "object" || usage === null) return null; - const usageRecord = usage as Record; - - const timestampMs = parseTimestampMs(record["timestamp"]); - if (timestampMs === null) return null; - - const model = typeof messageRecord["model"] === "string" ? messageRecord["model"] : ""; - if (model.length === 0) return null; - - const messageId = typeof messageRecord["id"] === "string" ? messageRecord["id"] : null; - const requestId = typeof record["requestId"] === "string" ? record["requestId"] : null; - // Matches ccusage: prefer the message/request pair, fall back to whichever - // half exists. Records with neither cannot be de-duplicated. - const dedupeKey = - messageId === null && requestId === null ? null : `${messageId ?? ""}:${requestId ?? ""}`; - - const cost = record["costUSD"]; - - return { - provider: "claude", - timestampMs, - model, - sessionId: typeof record["sessionId"] === "string" ? record["sessionId"] : "", - totals: { - uncachedInputTokens: int(usageRecord["input_tokens"]), - cachedInputTokens: int(usageRecord["cache_read_input_tokens"]), - cacheCreationTokens: int(usageRecord["cache_creation_input_tokens"]), - outputTokens: int(usageRecord["output_tokens"]), - // Anthropic folds thinking tokens into output and does not break them out. - reasoningTokens: 0, - }, - reportedCostUsd: typeof cost === "number" && Number.isFinite(cost) ? cost : null, - speed: usageRecord["speed"] === "fast" ? "fast" : "standard", - dedupeKey, - }; -} - -/* -------------------------------------------------------------------------- */ -/* Codex */ -/* -------------------------------------------------------------------------- */ - -/** - * Rolling state for a single Codex rollout file. - * - * Codex `token_count` events carry no model or service tier, so both are - * carried forward: the model from the most recent `turn_context`, the tier from - * the most recent `thread_settings_applied`. Sessions that switch either - * mid-run attribute correctly from the switch onward. - */ -export interface CodexScanState { - model: string; - speed: UsageSpeed; - sessionId: string; - lastUsageSignature: string | null; - sawSessionMeta: boolean; - /** While true, leading usage events are re-stamped copies of parent history. */ - suppressingForkCopies: boolean; - forkCopyAnchorMs: number; -} - -export function initialCodexScanState(): CodexScanState { - return { - model: "", - speed: "standard", - sessionId: "", - lastUsageSignature: null, - sawSessionMeta: false, - suppressingForkCopies: false, - forkCopyAnchorMs: 0, - }; -} - -/** - * A forked or subagent rollout opens with the parent's full history copied in, - * every line re-stamped to the fork instant. Those copies are written in one - * synchronous burst (observed gaps 0-40ms), while the child's first genuine - * usage event only lands after a real model turn (observed 5s+). One second of - * separation splits the two cleanly; `ccusage` uses the same threshold. - */ -const FORK_COPY_MAX_GAP_MS = 1000; - -/** Whether a `session_meta` payload marks the rollout as a fork or subagent. */ -function isForkedSessionMeta(payload: Record): boolean { - if (typeof payload["forked_from_id"] === "string") return true; - const source = payload["source"]; - if (typeof source !== "object" || source === null) return false; - const subagent = (source as Record)["subagent"]; - if (typeof subagent !== "object" || subagent === null) return false; - const spawn = (subagent as Record)["thread_spawn"]; - if (typeof spawn !== "object" || spawn === null) return false; - return typeof (spawn as Record)["parent_thread_id"] === "string"; -} - -/** - * Feeds one line of a Codex rollout into `state`, returning a record when the - * line was a usage event. - * - * Deltas come from `last_token_usage`. Summing those across a session - * reconciles with the session's final `total_token_usage`, provided - * consecutive duplicate events are dropped, which this does. - */ -export function parseCodexLine(line: string, state: CodexScanState): UsageRecord | null { - let parsed: unknown; - try { - parsed = JSON.parse(line); - } catch { - return null; - } - return parseCodexRecord(parsed, state); -} - -export function parseCodexRecord(parsed: unknown, state: CodexScanState): UsageRecord | null { - if (typeof parsed !== "object" || parsed === null) return null; - - const record = parsed as Record; - const payload = record["payload"]; - if (typeof payload !== "object" || payload === null) return null; - const payloadRecord = payload as Record; - const payloadType = payloadRecord["type"]; - - if (record["type"] === "session_meta") { - // Only the first meta describes this file's own session. A forked rollout - // repeats the ancestors' metas right after it; letting those through would - // reassign every subsequent record to an ancestor session. - if (state.sawSessionMeta) return null; - state.sawSessionMeta = true; - const id = payloadRecord["id"] ?? payloadRecord["session_id"]; - if (typeof id === "string") state.sessionId = id; - const metaTimestampMs = parseTimestampMs(record["timestamp"]); - if (metaTimestampMs !== null && isForkedSessionMeta(payloadRecord)) { - state.suppressingForkCopies = true; - state.forkCopyAnchorMs = metaTimestampMs; - } - return null; - } - - if (record["type"] === "turn_context") { - if (typeof payloadRecord["model"] === "string") state.model = payloadRecord["model"]; - return null; - } - - if (payloadType === "thread_settings_applied") { - const settings = payloadRecord["thread_settings"]; - if (typeof settings === "object" && settings !== null) { - state.speed = codexSpeed((settings as Record)["service_tier"]); - } - return null; - } - - if (payloadType !== "token_count") return null; - - const info = payloadRecord["info"]; - if (typeof info !== "object" || info === null) return null; - const last = (info as Record)["last_token_usage"]; - if (typeof last !== "object" || last === null) return null; - const lastRecord = last as Record; - - // Only an event that is otherwise eligible may consume the duplicate - // signature. A token_count arriving before its turn_context (no model yet) - // must not poison it, or the re-emitted copy after the model is known would - // be skipped as a duplicate and those tokens never counted. - const timestampMs = parseTimestampMs(record["timestamp"]); - if (timestampMs === null) return null; - if (state.model.length === 0) return null; - - // Codex re-emits an unchanged token_count on some stream boundaries. Summing - // those would double count, so identical consecutive payloads are skipped. - const signature = JSON.stringify(lastRecord); - if (signature === state.lastUsageSignature) return null; - state.lastUsageSignature = signature; - - // In a forked rollout the copied parent history was already counted from the - // parent's own file. Drop the leading burst; the first usage event separated - // from its predecessor by a real turn's worth of time ends it for good. - if (state.suppressingForkCopies) { - if (timestampMs - state.forkCopyAnchorMs < FORK_COPY_MAX_GAP_MS) { - state.forkCopyAnchorMs = timestampMs; - return null; - } - state.suppressingForkCopies = false; - } - - const inputTokens = int(lastRecord["input_tokens"]); - const cachedInputTokens = int(lastRecord["cached_input_tokens"]); - const cacheCreationTokens = int(lastRecord["cache_write_input_tokens"]); - const outputTokens = int(lastRecord["output_tokens"]); - - const totals: UsageTokenTotals = { - // Codex reports `input_tokens` inclusive of the cached portion. - uncachedInputTokens: Math.max(0, inputTokens - cachedInputTokens - cacheCreationTokens), - cachedInputTokens, - cacheCreationTokens, - outputTokens, - // Reported inside output_tokens, surfaced separately for the token mix. - reasoningTokens: Math.min(outputTokens, int(lastRecord["reasoning_output_tokens"])), - }; - - if (totalTokens(totals) === 0) return null; - - return { - provider: "codex", - timestampMs, - model: state.model, - sessionId: state.sessionId, - totals, - // Codex does not report cost in the rollout. - reportedCostUsd: null, - speed: state.speed, - // Events surviving the fork-copy suppression above are unique to this - // rollout, so they need no global dedup. - dedupeKey: null, - }; -} - -/** - * Maps a Codex `service_tier` to its billing speed. Codex omits the field when - * no tier was requested, which bills as standard, as do `default` and - * `standard`. `fast` is accepted as an alias of `priority`. - */ -function codexSpeed(serviceTier: unknown): UsageSpeed { - if (serviceTier === "priority" || serviceTier === "fast") return "fast"; - if (serviceTier === "ultrafast") return "ultrafast"; - return "standard"; -} - -/* -------------------------------------------------------------------------- */ -/* Grok Build */ -/* -------------------------------------------------------------------------- */ - -interface GrokUsageTotals { - readonly inputTokens: number; - readonly outputTokens: number; - readonly cachedReadTokens: number; - readonly cacheCreationTokens: number; - readonly reasoningTokens: number; - readonly costUsdTicks: number | null; -} - -function readGrokUsageTotals(value: unknown): GrokUsageTotals | null { - if (typeof value !== "object" || value === null) return null; - const record = value as Record; - return { - inputTokens: int(record["inputTokens"]), - outputTokens: int(record["outputTokens"]), - cachedReadTokens: int(record["cachedReadTokens"]), - cacheCreationTokens: int(record["cacheCreationTokens"]), - reasoningTokens: int(record["reasoningTokens"]), - costUsdTicks: - typeof record["costUsdTicks"] === "number" && Number.isFinite(record["costUsdTicks"]) - ? record["costUsdTicks"] - : null, - }; -} - -function grokTotalsToUsage(totals: GrokUsageTotals): UsageTokenTotals { - const cachedInputTokens = totals.cachedReadTokens; - const cacheCreationTokens = totals.cacheCreationTokens; - // Grok reports `inputTokens` inclusive of the cached portion, matching Codex. - const uncachedInputTokens = Math.max( - 0, - totals.inputTokens - cachedInputTokens - cacheCreationTokens, - ); - const outputTokens = totals.outputTokens; - return { - uncachedInputTokens, - cachedInputTokens, - cacheCreationTokens, - outputTokens, - reasoningTokens: Math.min(outputTokens, totals.reasoningTokens), - }; -} - -/** - * Parses one line of a Grok Build `updates.jsonl` session log. - * - * Usage lands on `turn_completed` session updates. Per-model breakdowns live - * under `usage.modelUsage`; when present each model becomes its own record. - * - * Returns every record for the line (0 or more). Callers stream line-by-line - * and flatten. - */ -export function parseGrokLine(line: string): readonly UsageRecord[] { - let parsed: unknown; - try { - parsed = JSON.parse(line); - } catch { - return []; - } - return parseGrokRecord(parsed); -} - -export function parseGrokRecord(parsed: unknown): readonly UsageRecord[] { - if (typeof parsed !== "object" || parsed === null) return []; - - const record = parsed as Record; - const params = record["params"]; - if (typeof params !== "object" || params === null) return []; - const paramsRecord = params as Record; - - const update = paramsRecord["update"]; - if (typeof update !== "object" || update === null) return []; - const updateRecord = update as Record; - if (updateRecord["sessionUpdate"] !== "turn_completed") return []; - - const usage = updateRecord["usage"]; - if (typeof usage !== "object" || usage === null) return []; - const usageRecord = usage as Record; - - const sessionId = typeof paramsRecord["sessionId"] === "string" ? paramsRecord["sessionId"] : ""; - const promptId = typeof updateRecord["prompt_id"] === "string" ? updateRecord["prompt_id"] : null; - - // Prefer the high-resolution agent clock; fall back to the outer unix seconds. - const meta = paramsRecord["_meta"]; - let timestampMs: number | null = null; - if (typeof meta === "object" && meta !== null) { - const agentTimestampMs = (meta as Record)["agentTimestampMs"]; - if (typeof agentTimestampMs === "number" && Number.isFinite(agentTimestampMs)) { - timestampMs = agentTimestampMs; - } - } - if (timestampMs === null) { - const timestamp = record["timestamp"]; - if (typeof timestamp === "number" && Number.isFinite(timestamp)) { - timestampMs = timestamp > 1e12 ? timestamp : timestamp * 1000; - } - } - if (timestampMs === null) return []; - - const topLevel = readGrokUsageTotals(usageRecord); - if (topLevel === null) return []; - - const modelUsage = usageRecord["modelUsage"]; - const modelEntries: Array<{ model: string; totals: GrokUsageTotals }> = []; - if (typeof modelUsage === "object" && modelUsage !== null) { - for (const [model, raw] of Object.entries(modelUsage as Record)) { - if (model.length === 0) continue; - const totals = readGrokUsageTotals(raw); - if (totals === null) continue; - modelEntries.push({ model, totals }); - } - } - - if (modelEntries.length === 0) { - if (totalTokens(grokTotalsToUsage(topLevel)) === 0) return []; - return [ - { - provider: "grok", - timestampMs, - model: "grok", - sessionId, - totals: grokTotalsToUsage(topLevel), - reportedCostUsd: grokCostTicksToUsd(topLevel.costUsdTicks), - speed: "standard", - // No prompt id means we cannot tell two same-second updates apart. - dedupeKey: promptId === null ? null : `${sessionId}:${promptId}:grok`, - }, - ]; - } - - // Cost allocation: - // 1. Emitted models with their own costUsdTicks keep those values. - // 2. Remaining aggregate cost (top-level minus those per-model ticks, - // clamped at 0) is pro-rated across emitted models that lack ticks, - // by token share among the unticked models only. - // 3. When no model has per-model ticks, remaining equals the full - // aggregate and every emitted model gets a token-share slice. - // Zero-token rows are never emitted and never count toward used ticks. - const topLevelCostUsd = grokCostTicksToUsd(topLevel.costUsdTicks); - let usedTickedCostUsd = 0; - let untickedTokenDenominator = 0; - for (const entry of modelEntries) { - const tokens = totalTokens(grokTotalsToUsage(entry.totals)); - if (tokens === 0) continue; - if (entry.totals.costUsdTicks !== null) { - usedTickedCostUsd += grokCostTicksToUsd(entry.totals.costUsdTicks) ?? 0; - } else { - untickedTokenDenominator += tokens; - } - } - const remainingCostUsd = - topLevelCostUsd === null ? null : Math.max(0, topLevelCostUsd - usedTickedCostUsd); - - const results: UsageRecord[] = []; - for (const entry of modelEntries) { - const totals = grokTotalsToUsage(entry.totals); - if (totalTokens(totals) === 0) continue; - - let reportedCostUsd = grokCostTicksToUsd(entry.totals.costUsdTicks); - if (reportedCostUsd === null && remainingCostUsd !== null && untickedTokenDenominator > 0) { - reportedCostUsd = remainingCostUsd * (totalTokens(totals) / untickedTokenDenominator); - } - - results.push({ - provider: "grok", - timestampMs, - model: entry.model, - sessionId, - totals, - reportedCostUsd, - speed: "standard", - dedupeKey: promptId === null ? null : `${sessionId}:${promptId}:${entry.model}`, - }); - } - return results; -} - -export { EMPTY_TOTALS }; diff --git a/packages/provider-core/package.json b/packages/provider-core/package.json index fdf4c7fd5288..c45de9a9b149 100644 --- a/packages/provider-core/package.json +++ b/packages/provider-core/package.json @@ -175,6 +175,10 @@ "types": "./src/server/threadTitleContext.ts", "import": "./src/server/threadTitleContext.ts" }, + "./server/usage": { + "types": "./src/server/usage.ts", + "import": "./src/server/usage.ts" + }, "./server/usageLimits": { "types": "./src/server/usageLimits.ts", "import": "./src/server/usageLimits.ts" diff --git a/packages/provider-core/src/server/driver.ts b/packages/provider-core/src/server/driver.ts index 18ed1aec1277..1801fa316901 100644 --- a/packages/provider-core/src/server/driver.ts +++ b/packages/provider-core/src/server/driver.ts @@ -42,6 +42,7 @@ import type { ProviderAuthController } from "./auth.ts"; import type { ProviderDriverError } from "./errors.ts"; import type { ManagedServerProvider } from "./snapshot.ts"; import type { ProviderTextGeneration } from "./textGeneration.ts"; +import type { ProviderUsageReader } from "./usage.ts"; /** * Static metadata advertised by a driver. Used for default presentation @@ -172,7 +173,7 @@ export interface ProviderDriverCreateInput { * scope closes. Two calls to `create` with different `instanceId` / * `config` MUST yield instances with no shared mutable state. */ -export interface ProviderDriver { +export interface ProviderDriver { readonly driverKind: ProviderDriverKind; readonly metadata: ProviderDriverMetadata; /** @@ -207,6 +208,11 @@ export interface ProviderDriver { readonly create: ( input: ProviderDriverCreateInput, ) => Effect.Effect; + /** + * Where the usage page reads this provider's token history, when it keeps + * one. `UsageR` is what the reader needs, usually far less than `R`. + */ + readonly usage?: ProviderUsageReader; } /** @@ -219,4 +225,8 @@ export interface ProviderDriver { // needs the original `Config` type. Using `unknown` instead would force // `create` callers into casts since `unknown` is not assignable to a // concrete `Config` from inside the driver body. -export type AnyProviderDriver = ProviderDriver; +export type AnyProviderDriver = ProviderDriver; + +/** The services a driver's usage reader needs. */ +export type ProviderUsageReaderEnv = + Driver extends ProviderDriver ? UsageR : never; diff --git a/packages/provider-core/src/server/usage.test.ts b/packages/provider-core/src/server/usage.test.ts new file mode 100644 index 000000000000..ea2cda92cbb9 --- /dev/null +++ b/packages/provider-core/src/server/usage.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "@effect/vitest"; + +import { totalTokens } from "./usage.ts"; + +describe("totalTokens", () => { + it("does not add reasoning on top of output", () => { + expect( + totalTokens({ + uncachedInputTokens: 10, + cachedInputTokens: 20, + cacheCreationTokens: 30, + outputTokens: 40, + reasoningTokens: 25, + }), + ).toBe(100); + }); +}); diff --git a/packages/provider-core/src/server/usage.ts b/packages/provider-core/src/server/usage.ts new file mode 100644 index 000000000000..f2cf52e67965 --- /dev/null +++ b/packages/provider-core/src/server/usage.ts @@ -0,0 +1,179 @@ +/** + * The usage reader contract: how a driver tells the server's usage page where + * its history lives and how to read it. + * + * A driver either points at JSONL transcripts that the server's shared engine + * streams, caches and resumes (`kind: "transcripts"`), or scans its own + * sources and returns their records (`kind: "scan"`). The server owns + * pricing, aggregation, de-duplication and the scan cache. + * + * @module provider-core/server/usage + */ +import type { + ProviderInstanceId, + UsageProviderKind, + UsageSource, + UsageTokenTotals, +} from "@t3tools/contracts"; +import type * as Effect from "effect/Effect"; +import type * as Schema from "effect/Schema"; + +/** + * Billing speed of a request. Faster speeds bill at a model-specific premium. + * Claude fast mode and Codex `priority` are `fast`; Codex `ultrafast` is its + * own, more expensive tier. + */ +export type UsageSpeed = "standard" | "fast" | "ultrafast"; + +export interface UsageRecord { + readonly provider: UsageProviderKind; + readonly timestampMs: number; + readonly model: string; + /** + * Rate-table key when the provider's display name carries tiers the table + * does not know, such as Cursor's `claude-opus-5-5-high`. Defaults to `model`. + */ + readonly rateModel?: string; + readonly sessionId: string; + readonly totals: UsageTokenTotals; + readonly reportedCostUsd: number | null; + /** Only Claude Code and Codex record a speed; other providers are `standard`. */ + readonly speed: UsageSpeed; + /** + * Key for cross-file de-duplication, or `null` when the record is inherently + * unique and needs no dedup. + */ + readonly dedupeKey: string | null; +} + +export const EMPTY_TOTALS: UsageTokenTotals = { + uncachedInputTokens: 0, + cachedInputTokens: 0, + cacheCreationTokens: 0, + outputTokens: 0, + reasoningTokens: 0, +}; + +export function totalTokens(totals: UsageTokenTotals): number { + // reasoningTokens is a subset of outputTokens and must not be added again. + return ( + totals.uncachedInputTokens + + totals.cachedInputTokens + + totals.cacheCreationTokens + + totals.outputTokens + ); +} + +/** A token count from untrusted JSON: a positive finite number, truncated, else 0. */ +export function tokenCount(value: unknown): number { + return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.trunc(value) : 0; +} + +/** Epoch milliseconds of an ISO timestamp string, or `null`. */ +export function parseTimestampMs(value: unknown): number | null { + if (typeof value !== "string") return null; + const parsed = Date.parse(value); + return Number.isNaN(parsed) ? null : parsed; +} + +/** JSON fields a format reads. `true` keeps the whole subtree. */ +export type SelectedFields = { readonly [key: string]: true | SelectedFields }; + +/** + * How the server's JSONL engine reads one provider's transcript lines. + * + * Lines arrive one at a time. A line too large to parse whole is projected to + * `selectFields` and handed to `parseProjected` instead of `parseLine`. + */ +export interface TranscriptUsageFormat { + readonly selectFields: SelectedFields; + /** + * Cheap substring gate applied before `JSON.parse`. Transcripts are mostly + * tool output; skipping lines that cannot matter is worth an order of + * magnitude. Lines that only update `State` must pass too. + */ + mightCarryUsage(line: string): boolean; + parseLine(line: string, state: State): readonly UsageRecord[]; + parseProjected(projected: unknown, state: State): readonly UsageRecord[]; + /** + * Present for formats whose records depend on earlier lines. The state at a + * resume point is persisted in the scan cache through `schema`, so a grown + * file resumes with it. Absent means every line stands alone. + */ + readonly state?: { + readonly initial: () => State; + readonly schema: Schema.Codec; + }; + /** + * Whether one session's records may appear in several files, such as a + * rollout copied when it moves. The server then keys those sessions' + * records for cross-file de-duplication. + */ + readonly sharedSessionsAcrossFiles?: true; +} + +/** A configured instance of the driver, its config already decoded by `driver.configSchema`. */ +export interface ProviderUsageInstance { + readonly instanceId: ProviderInstanceId; + /** + * `undefined` when the stored config does not decode. The instance still has + * history on disk, so readers that need no config read it anyway. + */ + readonly config: Config | undefined; + /** The instance's environment merged over the host environment. */ + readonly environment: NodeJS.ProcessEnv; + /** Whether settings list the instance, rather than it being the implicit default. */ + readonly configured: boolean; +} + +/** One source a `scan` reader read. */ +export interface ProviderUsageScan { + readonly dir: string; + readonly volumeId: string; + /** Overrides the server's host name in the source fingerprint, for account-wide sources. */ + readonly hostId?: string; + readonly status?: UsageSource["status"]; + readonly message?: string; + readonly action?: UsageSource["action"]; + /** Parsed records per file, or `null` when the source does not exist. */ + readonly files: + | readonly { readonly path: string; readonly records: readonly UsageRecord[] }[] + | null; + /** Answered from a cache while a refresh runs. */ + readonly refreshing?: true; +} + +export interface ProviderUsageScanInput { + readonly instances: ReadonlyArray>; + /** Files last written before this cannot hold records inside the window. */ + readonly windowStartMs: number; + /** The oldest usage the server keeps; account caches cover back to here. */ + readonly retentionCutoffMs: number; + /** Wait for a slow refresh instead of answering from a cache. */ + readonly awaitRefresh: boolean; +} + +export type ProviderUsageReader = + | { + readonly kind: "transcripts"; + readonly provider: UsageProviderKind; + readonly format: TranscriptUsageFormat; + /** + * Transcript directories for one instance. The server canonicalizes, + * de-duplicates and scans them; `fileName` limits a walk to one basename. + */ + readonly directories: ( + instance: ProviderUsageInstance, + ) => Effect.Effect< + ReadonlyArray<{ readonly dir: string; readonly fileName?: string }>, + never, + R + >; + } + | { + readonly kind: "scan"; + readonly provider: UsageProviderKind; + readonly scan: ( + input: ProviderUsageScanInput, + ) => Effect.Effect, never, R>; + }; diff --git a/packages/provider-grok/package.json b/packages/provider-grok/package.json index f72b76a11dfe..95ba20214e81 100644 --- a/packages/provider-grok/package.json +++ b/packages/provider-grok/package.json @@ -15,6 +15,10 @@ "types": "./src/server.ts", "import": "./src/server.ts" }, + "./server/usage": { + "types": "./src/server/usage.ts", + "import": "./src/server/usage.ts" + }, "./testing": { "types": "./src/testing.ts", "import": "./src/testing.ts" diff --git a/packages/provider-grok/src/server/driver.ts b/packages/provider-grok/src/server/driver.ts index 9846ba40a903..62ae7c771ed5 100644 --- a/packages/provider-grok/src/server/driver.ts +++ b/packages/provider-grok/src/server/driver.ts @@ -19,6 +19,7 @@ import { checkGrokProviderStatus, enrichGrokSnapshot, } from "./status.ts"; +import { grokUsageReader } from "./usage.ts"; import { readGrokAccount } from "./usageLimits.ts"; import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider"; import { @@ -82,7 +83,7 @@ export type GrokDriverEnv = | Path.Path | ProviderEventLoggers.ProviderEventLoggers; -export const GrokDriver: ProviderDriver = { +export const GrokDriver: ProviderDriver = { driverKind: DRIVER_KIND, metadata: { displayName: "Grok", @@ -90,6 +91,7 @@ export const GrokDriver: ProviderDriver = { }, configSchema: GrokSettings, defaultConfig: (): GrokSettings => decodeGrokSettings({}), + usage: grokUsageReader, create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => Effect.gen(function* () { const crypto = yield* Crypto.Crypto; diff --git a/packages/provider-grok/src/server/usage.test.ts b/packages/provider-grok/src/server/usage.test.ts new file mode 100644 index 000000000000..d4b6122dbafb --- /dev/null +++ b/packages/provider-grok/src/server/usage.test.ts @@ -0,0 +1,316 @@ +import { describe, expect, it } from "@effect/vitest"; + +import { GROK_COST_USD_TICKS_PER_DOLLAR, parseGrokLine } from "./usage.ts"; + +describe("parseGrokLine", () => { + /** Shaped after a real Grok Build `turn_completed` session update. */ + function turnCompleted(overrides?: { + sessionId?: string; + promptId?: string; + timestamp?: number; + agentTimestampMs?: number; + usage?: Record; + modelUsage?: Record> | null; + }): string { + const modelUsage = + overrides && "modelUsage" in overrides + ? overrides.modelUsage + : { + "grok-4.5-build": { + inputTokens: 20_272, + outputTokens: 272, + totalTokens: 20_544, + cachedReadTokens: 11_264, + cacheCreationTokens: 0, + reasoningTokens: 180, + costUsdTicks: 230_272_000, + }, + }; + + return JSON.stringify({ + timestamp: overrides?.timestamp ?? 1_786_372_566, + method: "_x.ai/session/update", + params: { + sessionId: overrides?.sessionId ?? "019fec1a-12f7-72f2-9b1f-7778a00aea3c", + update: { + sessionUpdate: "turn_completed", + prompt_id: overrides?.promptId ?? "prompt-1", + stop_reason: "end_turn", + usage: { + inputTokens: 20_272, + outputTokens: 272, + totalTokens: 20_544, + cachedReadTokens: 11_264, + cacheCreationTokens: 0, + reasoningTokens: 180, + costUsdTicks: 230_272_000, + ...(modelUsage === null ? {} : { modelUsage }), + ...overrides?.usage, + }, + }, + _meta: { + eventId: "event-1", + agentTimestampMs: overrides?.agentTimestampMs ?? 1_786_372_566_485, + }, + }, + }); + } + + it("extracts per-model totals and provider-reported cost ticks", () => { + const records = parseGrokLine(turnCompleted()); + + expect(records).toHaveLength(1); + const [record] = records; + expect(record?.provider).toBe("grok"); + expect(record?.model).toBe("grok-4.5-build"); + expect(record?.sessionId).toBe("019fec1a-12f7-72f2-9b1f-7778a00aea3c"); + expect(record?.timestampMs).toBe(1_786_372_566_485); + expect(record?.totals).toEqual({ + uncachedInputTokens: 20_272 - 11_264, + cachedInputTokens: 11_264, + cacheCreationTokens: 0, + outputTokens: 272, + reasoningTokens: 180, + }); + expect(record?.reportedCostUsd).toBeCloseTo(230_272_000 / GROK_COST_USD_TICKS_PER_DOLLAR, 12); + expect(record?.dedupeKey).toBe("019fec1a-12f7-72f2-9b1f-7778a00aea3c:prompt-1:grok-4.5-build"); + }); + + it("emits one record per model when modelUsage has several entries", () => { + const records = parseGrokLine( + turnCompleted({ + modelUsage: { + "grok-4.5": { + inputTokens: 1000, + outputTokens: 50, + cachedReadTokens: 400, + reasoningTokens: 20, + costUsdTicks: 50_000_000, + }, + "grok-composer-2.5-fast": { + inputTokens: 200, + outputTokens: 30, + cachedReadTokens: 100, + reasoningTokens: 0, + costUsdTicks: 10_000_000, + }, + }, + }), + ); + + expect(records.map((record) => record.model).toSorted()).toEqual([ + "grok-4.5", + "grok-composer-2.5-fast", + ]); + expect(records.every((record) => record.provider === "grok")).toBe(true); + expect(records.find((record) => record.model === "grok-4.5")?.reportedCostUsd).toBeCloseTo( + 0.005, + 12, + ); + }); + + it("inherits top-level cost ticks for a single model without its own ticks", () => { + const records = parseGrokLine( + turnCompleted({ + modelUsage: { + "grok-4.5-build": { + inputTokens: 1000, + outputTokens: 10, + cachedReadTokens: 0, + reasoningTokens: 0, + }, + }, + usage: { costUsdTicks: GROK_COST_USD_TICKS_PER_DOLLAR }, + }), + ); + + expect(records).toHaveLength(1); + expect(records[0]?.reportedCostUsd).toBe(1); + }); + + it("falls back to a generic grok model when modelUsage is absent", () => { + const records = parseGrokLine(turnCompleted({ modelUsage: null })); + + expect(records).toHaveLength(1); + const [record] = records; + expect(record?.provider).toBe("grok"); + expect(record?.model).toBe("grok"); + expect(record?.totals).toEqual({ + uncachedInputTokens: 20_272 - 11_264, + cachedInputTokens: 11_264, + cacheCreationTokens: 0, + outputTokens: 272, + reasoningTokens: 180, + }); + expect(record?.reportedCostUsd).toBeCloseTo(230_272_000 / GROK_COST_USD_TICKS_PER_DOLLAR, 12); + expect(record?.dedupeKey).toBe("019fec1a-12f7-72f2-9b1f-7778a00aea3c:prompt-1:grok"); + }); + + it("pro-rates top-level cost ticks across multi-model turns without per-model ticks", () => { + const records = parseGrokLine( + turnCompleted({ + modelUsage: { + "grok-4.5": { + inputTokens: 300, + outputTokens: 0, + cachedReadTokens: 0, + reasoningTokens: 0, + }, + "grok-composer-2.5-fast": { + inputTokens: 100, + outputTokens: 0, + cachedReadTokens: 0, + reasoningTokens: 0, + }, + }, + usage: { costUsdTicks: GROK_COST_USD_TICKS_PER_DOLLAR }, + }), + ); + + expect(records).toHaveLength(2); + const byModel = Object.fromEntries(records.map((record) => [record.model, record])); + expect(byModel["grok-4.5"]?.reportedCostUsd).toBeCloseTo(0.75, 12); + expect(byModel["grok-composer-2.5-fast"]?.reportedCostUsd).toBeCloseTo(0.25, 12); + const sum = + (byModel["grok-4.5"]?.reportedCostUsd ?? 0) + + (byModel["grok-composer-2.5-fast"]?.reportedCostUsd ?? 0); + expect(sum).toBeCloseTo(1, 12); + }); + + it("pro-rates aggregate cost when a zero-token sibling carries costUsdTicks: 0", () => { + const records = parseGrokLine( + turnCompleted({ + modelUsage: { + "grok-4.5": { + inputTokens: 300, + outputTokens: 0, + cachedReadTokens: 0, + reasoningTokens: 0, + }, + "grok-composer-2.5-fast": { + inputTokens: 100, + outputTokens: 0, + cachedReadTokens: 0, + reasoningTokens: 0, + }, + "empty-sibling": { + inputTokens: 0, + outputTokens: 0, + cachedReadTokens: 0, + reasoningTokens: 0, + costUsdTicks: 0, + }, + }, + usage: { costUsdTicks: GROK_COST_USD_TICKS_PER_DOLLAR }, + }), + ); + + expect(records).toHaveLength(2); + expect(records.every((record) => record.model !== "empty-sibling")).toBe(true); + const byModel = Object.fromEntries(records.map((record) => [record.model, record])); + expect(byModel["grok-4.5"]?.reportedCostUsd).toBeCloseTo(0.75, 12); + expect(byModel["grok-composer-2.5-fast"]?.reportedCostUsd).toBeCloseTo(0.25, 12); + const sum = + (byModel["grok-4.5"]?.reportedCostUsd ?? 0) + + (byModel["grok-composer-2.5-fast"]?.reportedCostUsd ?? 0); + expect(sum).toBeCloseTo(1, 12); + }); + + it("allocates leftover aggregate ticks to models that omit per-model ticks", () => { + const records = parseGrokLine( + turnCompleted({ + modelUsage: { + "grok-4.5": { + inputTokens: 300, + outputTokens: 0, + cachedReadTokens: 0, + reasoningTokens: 0, + costUsdTicks: 0.4 * GROK_COST_USD_TICKS_PER_DOLLAR, + }, + "grok-composer-2.5-fast": { + inputTokens: 100, + outputTokens: 0, + cachedReadTokens: 0, + reasoningTokens: 0, + }, + }, + usage: { costUsdTicks: GROK_COST_USD_TICKS_PER_DOLLAR }, + }), + ); + + expect(records).toHaveLength(2); + const byModel = Object.fromEntries(records.map((record) => [record.model, record])); + expect(byModel["grok-4.5"]?.reportedCostUsd).toBeCloseTo(0.4, 12); + expect(byModel["grok-composer-2.5-fast"]?.reportedCostUsd).toBeCloseTo(0.6, 12); + const sum = + (byModel["grok-4.5"]?.reportedCostUsd ?? 0) + + (byModel["grok-composer-2.5-fast"]?.reportedCostUsd ?? 0); + expect(sum).toBeCloseTo(1, 12); + }); + + it("does not invent a colliding dedupe key when prompt_id is missing", () => { + const line = JSON.stringify({ + timestamp: 1_786_372_566, + method: "_x.ai/session/update", + params: { + sessionId: "s1", + update: { + sessionUpdate: "turn_completed", + usage: { + inputTokens: 10, + outputTokens: 2, + modelUsage: { + "grok-4.5": { inputTokens: 10, outputTokens: 2 }, + }, + }, + }, + }, + }); + + expect(parseGrokLine(line)[0]?.dedupeKey).toBeNull(); + }); + + it("ignores non-turn lines and empty usage", () => { + expect(parseGrokLine(JSON.stringify({ method: "session/update", params: {} }))).toEqual([]); + expect(parseGrokLine("not json")).toEqual([]); + expect( + parseGrokLine( + turnCompleted({ + modelUsage: { + "grok-4.5-build": { + inputTokens: 0, + outputTokens: 0, + cachedReadTokens: 0, + reasoningTokens: 0, + costUsdTicks: 0, + }, + }, + }), + ), + ).toEqual([]); + }); + + it("falls back to the outer unix-seconds timestamp when agent meta is missing", () => { + const line = JSON.stringify({ + timestamp: 1_786_372_566, + method: "_x.ai/session/update", + params: { + sessionId: "s1", + update: { + sessionUpdate: "turn_completed", + prompt_id: "p1", + usage: { + inputTokens: 10, + outputTokens: 2, + modelUsage: { + "grok-4.5": { inputTokens: 10, outputTokens: 2 }, + }, + }, + }, + }, + }); + + const records = parseGrokLine(line); + expect(records[0]?.timestampMs).toBe(1_786_372_566_000); + }); +}); diff --git a/packages/provider-grok/src/server/usage.ts b/packages/provider-grok/src/server/usage.ts new file mode 100644 index 000000000000..9f2be3c04299 --- /dev/null +++ b/packages/provider-grok/src/server/usage.ts @@ -0,0 +1,239 @@ +/** + * Usage history for Grok Build: the format of its per-session + * `updates.jsonl` logs and where each instance's `GROK_HOME` keeps them. + * + * @module provider-grok/server/usage + */ +import * as NodeOS from "node:os"; + +import type { UsageTokenTotals } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Path from "effect/Path"; + +import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; +import { + tokenCount, + totalTokens, + type ProviderUsageReader, + type TranscriptUsageFormat, + type UsageRecord, +} from "@t3tools/provider-core/server/usage"; + +import type { GrokSettings } from "../settings.ts"; + +/** + * Grok reports cost in integer ticks where `1 USD = 10^10` ticks. See Grok + * headless `total_cost_usd_ticks`. Convert to dollars for pricing. + */ +export const GROK_COST_USD_TICKS_PER_DOLLAR = 10_000_000_000; + +function grokCostTicksToUsd(ticks: unknown): number | null { + if (typeof ticks !== "number" || !Number.isFinite(ticks) || ticks < 0) return null; + return ticks / GROK_COST_USD_TICKS_PER_DOLLAR; +} + +interface GrokUsageTotals { + readonly inputTokens: number; + readonly outputTokens: number; + readonly cachedReadTokens: number; + readonly cacheCreationTokens: number; + readonly reasoningTokens: number; + readonly costUsdTicks: number | null; +} + +function readGrokUsageTotals(value: unknown): GrokUsageTotals | null { + if (typeof value !== "object" || value === null) return null; + const record = value as Record; + return { + inputTokens: tokenCount(record["inputTokens"]), + outputTokens: tokenCount(record["outputTokens"]), + cachedReadTokens: tokenCount(record["cachedReadTokens"]), + cacheCreationTokens: tokenCount(record["cacheCreationTokens"]), + reasoningTokens: tokenCount(record["reasoningTokens"]), + costUsdTicks: + typeof record["costUsdTicks"] === "number" && Number.isFinite(record["costUsdTicks"]) + ? record["costUsdTicks"] + : null, + }; +} + +function grokTotalsToUsage(totals: GrokUsageTotals): UsageTokenTotals { + const cachedInputTokens = totals.cachedReadTokens; + const cacheCreationTokens = totals.cacheCreationTokens; + // Grok reports `inputTokens` inclusive of the cached portion, matching Codex. + const uncachedInputTokens = Math.max( + 0, + totals.inputTokens - cachedInputTokens - cacheCreationTokens, + ); + const outputTokens = totals.outputTokens; + return { + uncachedInputTokens, + cachedInputTokens, + cacheCreationTokens, + outputTokens, + reasoningTokens: Math.min(outputTokens, totals.reasoningTokens), + }; +} + +/** + * Parses one line of a Grok Build `updates.jsonl` session log. + * + * Usage lands on `turn_completed` session updates. Per-model breakdowns live + * under `usage.modelUsage`; when present each model becomes its own record. + * + * Returns every record for the line (0 or more). Callers stream line-by-line + * and flatten. + */ +export function parseGrokLine(line: string): readonly UsageRecord[] { + let parsed: unknown; + try { + parsed = JSON.parse(line); + } catch { + return []; + } + return parseGrokRecord(parsed); +} + +function parseGrokRecord(parsed: unknown): readonly UsageRecord[] { + if (typeof parsed !== "object" || parsed === null) return []; + + const record = parsed as Record; + const params = record["params"]; + if (typeof params !== "object" || params === null) return []; + const paramsRecord = params as Record; + + const update = paramsRecord["update"]; + if (typeof update !== "object" || update === null) return []; + const updateRecord = update as Record; + if (updateRecord["sessionUpdate"] !== "turn_completed") return []; + + const usage = updateRecord["usage"]; + if (typeof usage !== "object" || usage === null) return []; + const usageRecord = usage as Record; + + const sessionId = typeof paramsRecord["sessionId"] === "string" ? paramsRecord["sessionId"] : ""; + const promptId = typeof updateRecord["prompt_id"] === "string" ? updateRecord["prompt_id"] : null; + + // Prefer the high-resolution agent clock; fall back to the outer unix seconds. + const meta = paramsRecord["_meta"]; + let timestampMs: number | null = null; + if (typeof meta === "object" && meta !== null) { + const agentTimestampMs = (meta as Record)["agentTimestampMs"]; + if (typeof agentTimestampMs === "number" && Number.isFinite(agentTimestampMs)) { + timestampMs = agentTimestampMs; + } + } + if (timestampMs === null) { + const timestamp = record["timestamp"]; + if (typeof timestamp === "number" && Number.isFinite(timestamp)) { + timestampMs = timestamp > 1e12 ? timestamp : timestamp * 1000; + } + } + if (timestampMs === null) return []; + + const topLevel = readGrokUsageTotals(usageRecord); + if (topLevel === null) return []; + + const modelUsage = usageRecord["modelUsage"]; + const modelEntries: Array<{ model: string; totals: GrokUsageTotals }> = []; + if (typeof modelUsage === "object" && modelUsage !== null) { + for (const [model, raw] of Object.entries(modelUsage as Record)) { + if (model.length === 0) continue; + const totals = readGrokUsageTotals(raw); + if (totals === null) continue; + modelEntries.push({ model, totals }); + } + } + + if (modelEntries.length === 0) { + if (totalTokens(grokTotalsToUsage(topLevel)) === 0) return []; + return [ + { + provider: "grok", + timestampMs, + model: "grok", + sessionId, + totals: grokTotalsToUsage(topLevel), + reportedCostUsd: grokCostTicksToUsd(topLevel.costUsdTicks), + speed: "standard", + // No prompt id means we cannot tell two same-second updates apart. + dedupeKey: promptId === null ? null : `${sessionId}:${promptId}:grok`, + }, + ]; + } + + // Cost allocation: + // 1. Emitted models with their own costUsdTicks keep those values. + // 2. Remaining aggregate cost (top-level minus those per-model ticks, + // clamped at 0) is pro-rated across emitted models that lack ticks, + // by token share among the unticked models only. + // 3. When no model has per-model ticks, remaining equals the full + // aggregate and every emitted model gets a token-share slice. + // Zero-token rows are never emitted and never count toward used ticks. + const topLevelCostUsd = grokCostTicksToUsd(topLevel.costUsdTicks); + let usedTickedCostUsd = 0; + let untickedTokenDenominator = 0; + for (const entry of modelEntries) { + const tokens = totalTokens(grokTotalsToUsage(entry.totals)); + if (tokens === 0) continue; + if (entry.totals.costUsdTicks !== null) { + usedTickedCostUsd += grokCostTicksToUsd(entry.totals.costUsdTicks) ?? 0; + } else { + untickedTokenDenominator += tokens; + } + } + const remainingCostUsd = + topLevelCostUsd === null ? null : Math.max(0, topLevelCostUsd - usedTickedCostUsd); + + const results: UsageRecord[] = []; + for (const entry of modelEntries) { + const totals = grokTotalsToUsage(entry.totals); + if (totalTokens(totals) === 0) continue; + + let reportedCostUsd = grokCostTicksToUsd(entry.totals.costUsdTicks); + if (reportedCostUsd === null && remainingCostUsd !== null && untickedTokenDenominator > 0) { + reportedCostUsd = remainingCostUsd * (totalTokens(totals) / untickedTokenDenominator); + } + + results.push({ + provider: "grok", + timestampMs, + model: entry.model, + sessionId, + totals, + reportedCostUsd, + speed: "standard", + dedupeKey: promptId === null ? null : `${sessionId}:${promptId}:${entry.model}`, + }); + } + return results; +} + +export const grokUsageFormat: TranscriptUsageFormat = { + selectFields: { + timestamp: true, + params: { + sessionId: true, + _meta: { agentTimestampMs: true }, + update: { sessionUpdate: true, prompt_id: true, usage: true }, + }, + }, + mightCarryUsage: (line) => line.includes('"turn_completed"'), + parseLine: (line) => parseGrokLine(line), + parseProjected: (projected) => parseGrokRecord(projected), +}; + +export const grokUsageReader: ProviderUsageReader = { + kind: "transcripts", + provider: "grok", + format: grokUsageFormat, + directories: Effect.fn("grokUsageReader.directories")(function* ({ environment }) { + const path = yield* Path.Path; + const home = expandHomePath( + environment.GROK_HOME?.trim() || path.join(NodeOS.homedir(), ".grok"), + ); + // Sessions also ship multi-megabyte `chat_history` and `events` logs that + // never carry usage; only `updates.jsonl` does. + return [{ dir: path.resolve(home, "sessions"), fileName: "updates.jsonl" }]; + }), +}; From 23957c704b43c25a253baf2644829e88cf45917f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 15:26:19 -0700 Subject: [PATCH 17/39] refactor(usage): OpenCode usage comes from provider-opencode (#17577) Co-authored-by: Claude Opus 5.5 --- apps/server/src/provider/builtInDrivers.ts | 5 +- apps/server/src/usage/UsageService.ts | 66 +++++++++-------- .../src/usage/usageTranscriptReader.test.ts | 50 ------------- packages/provider-core/src/server/usage.ts | 3 +- .../provider-opencode/src/server/driver.ts | 8 ++- .../src/server/usage.test.ts | 71 ++++++++++++++++++ .../provider-opencode/src/server/usage.ts | 72 ++++++++++++++++++- 7 files changed, 191 insertions(+), 84 deletions(-) create mode 100644 packages/provider-opencode/src/server/usage.test.ts rename apps/server/src/usage/opencodeUsageReader.ts => packages/provider-opencode/src/server/usage.ts (73%) diff --git a/apps/server/src/provider/builtInDrivers.ts b/apps/server/src/provider/builtInDrivers.ts index 4cecbc6ca50b..4576f4ea36bb 100644 --- a/apps/server/src/provider/builtInDrivers.ts +++ b/apps/server/src/provider/builtInDrivers.ts @@ -74,7 +74,8 @@ export const BUILT_IN_DRIVERS: ReadonlyArray | ProviderUsageReaderEnv - | ProviderUsageReaderEnv; + | ProviderUsageReaderEnv + | ProviderUsageReaderEnv; /** * The drivers that keep usage history, in the order the usage page reads @@ -83,4 +84,4 @@ export type BuiltInUsageReadersEnv = */ export const BUILT_IN_USAGE_DRIVERS: ReadonlyArray< AnyProviderDriver -> = [ClaudeDriver, CodexDriver, GrokDriver]; +> = [ClaudeDriver, CodexDriver, GrokDriver, OpenCodeDriver]; diff --git a/apps/server/src/usage/UsageService.ts b/apps/server/src/usage/UsageService.ts index 94d967286f45..c8a1891ce8f8 100644 --- a/apps/server/src/usage/UsageService.ts +++ b/apps/server/src/usage/UsageService.ts @@ -63,7 +63,6 @@ import type { TranscriptUsageFormat, UsageRecord, } from "@t3tools/provider-core/server/usage"; -import { readOpenCodeUsage } from "./opencodeUsageReader.ts"; import { makeAntigravityUsageCache, readAntigravityUsage } from "./antigravityUsageReader.ts"; import { CURSOR_ACCOUNT_CACHE_FILE_NAME, @@ -127,6 +126,11 @@ const transcriptReaders = BUILT_IN_USAGE_DRIVERS.flatMap((driver) => driver.usage?.kind === "transcripts" ? [{ driver, reader: driver.usage }] : [], ); +/** The scan readers, in driver order. */ +const scanReaders = BUILT_IN_USAGE_DRIVERS.flatMap((driver) => + driver.usage?.kind === "scan" ? [{ driver, reader: driver.usage }] : [], +); + /** Transcript formats by provider, for decoding the persisted scan cache. */ const transcriptFormats = new Map( transcriptReaders.map(({ reader }) => [reader.provider, reader.format] as const), @@ -851,32 +855,36 @@ export const make = Effect.gen(function* () { } return [...canonical]; }); - const dataHome = hostEnvironment["XDG_DATA_HOME"]?.trim(); - const openCode = Effect.gen(function* () { - const roots = yield* envRoots("OPENCODE_DATA_DIR", [ - path.join( - dataHome && path.isAbsolute(dataHome) ? dataHome : path.join(home, ".local", "share"), - "opencode", - ), - ]); - return yield* Effect.forEach( - roots, - (dir) => - Effect.gen(function* () { - const result = yield* Effect.promise(() => readOpenCodeUsage(dir, windowStartMs)); - return { - provider: "opencode", - dir, - volumeId: yield* Effect.promise(() => readDirectoryVolumeId(dir)), - files: result.missing && !result.error ? null : result.files, - status: result.error ? "partial" : "ok", - ...(result.error ? { message: "Some OpenCode history could not be read." } : {}), - } satisfies ScannedDir; - }), - { concurrency: "unbounded" }, - ); - }); + const scans = Effect.forEach( + scanReaders, + ({ driver, reader }) => + reader + .scan({ + instances: usageInstances(driver, settings), + windowStartMs, + retentionCutoffMs, + awaitRefresh, + }) + .pipe( + Effect.flatMap((sources) => + Effect.forEach(sources, ({ volumeId, ...source }) => + Effect.map( + volumeId === undefined + ? Effect.promise(() => readDirectoryVolumeId(source.dir)) + : Effect.succeed(volumeId), + (resolved): ScannedDir => ({ + ...source, + provider: reader.provider, + volumeId: resolved, + }), + ), + ), + ), + Effect.provideContext(readerContext), + ), + { concurrency: "unbounded" }, + ).pipe(Effect.map((sources) => sources.flat())); const antigravity = Effect.gen(function* () { const antigravityRoots = yield* envRoots("ANTIGRAVITY_DATA_DIR", [ @@ -999,10 +1007,10 @@ export const make = Effect.gen(function* () { // Independent sources scan together. Transcript directories go one at a // time, so open files stay at `TRANSCRIPT_READ_CONCURRENCY`. The result // keeps this order, since aggregation keeps the first copy of a duplicate. - const [transcripts, openCodeDirs, antigravityDirs, cursorDirs] = yield* Effect.all( + const [transcripts, scanDirs, antigravityDirs, cursorDirs] = yield* Effect.all( [ Effect.forEach(dirs, (dir) => scanTranscriptDir(dir, windowStartMs)), - openCode, + scans, antigravity, cursor, ], @@ -1010,7 +1018,7 @@ export const make = Effect.gen(function* () { ); const scanned: readonly ScannedDir[] = [ ...transcripts, - ...openCodeDirs, + ...scanDirs, ...antigravityDirs, ...cursorDirs, ]; diff --git a/apps/server/src/usage/usageTranscriptReader.test.ts b/apps/server/src/usage/usageTranscriptReader.test.ts index 183424cce3a7..60fdc8f27367 100644 --- a/apps/server/src/usage/usageTranscriptReader.test.ts +++ b/apps/server/src/usage/usageTranscriptReader.test.ts @@ -10,7 +10,6 @@ import { afterEach, assert, beforeEach, describe, it } from "@effect/vitest"; import { TEST_FORMATS } from "./usageTestFormats.ts"; import { readTranscriptRecords } from "./usageTranscriptReader.ts"; -import { readOpenCodeUsage } from "./opencodeUsageReader.ts"; import { readCursorAccountUsage } from "./cursorUsageReader.ts"; import { makeAntigravityUsageCache, readAntigravityUsage } from "./antigravityUsageReader.ts"; @@ -482,55 +481,6 @@ describe("SQLite usage readers", () => { assert.isFalse(requested); }); - it("counts migrated OpenCode messages once and sees subsequent WAL writes", async () => { - const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "opencode.db")); - try { - db.exec( - "PRAGMA journal_mode = WAL; PRAGMA wal_autocheckpoint = 0; CREATE TABLE message (id TEXT, session_id TEXT, data TEXT)", - ); - const message = { - id: "msg-1", - sessionID: "session-1", - role: "assistant", - modelID: "claude-sonnet-4-5", - time: { created: 1780000000000 }, - cost: 0.25, - tokens: { input: 100, output: 20, reasoning: 5, cache: { read: 30, write: 10 } }, - }; - const insert = db.prepare("INSERT INTO message VALUES (?, ?, ?)"); - insert.run(message.id, message.sessionID, JSON.stringify(message)); - const legacy = NodePath.join(dir, "storage", "message", message.sessionID); - await NodeFSP.mkdir(legacy, { recursive: true }); - await NodeFSP.writeFile(NodePath.join(legacy, "msg-1.json"), JSON.stringify(message)); - const first = await readOpenCodeUsage(dir, 0); - assert.isFalse(first.error); - const records = first.files.flatMap((file) => file.records); - assert.strictEqual(records.length, 1); - assert.deepStrictEqual(records[0]?.totals, { - uncachedInputTokens: 100, - cachedInputTokens: 30, - cacheCreationTokens: 10, - outputTokens: 25, - reasoningTokens: 5, - }); - assert.strictEqual(records[0]?.reportedCostUsd, 0.25); - insert.run( - "msg-2", - message.sessionID, - JSON.stringify({ ...message, id: "msg-2", time: { created: 1780000001000 } }), - ); - const next = await readOpenCodeUsage(dir, 1780000001000); - assert.isFalse(next.error); - assert.deepStrictEqual( - next.files.flatMap((file) => file.records).map((record) => record.dedupeKey), - ["opencode:msg-2"], - ); - assert.isAbove((await NodeFSP.stat(NodePath.join(dir, "opencode.db-wal"))).size, 0); - } finally { - db.close(); - } - }); - it("deduplicates Antigravity generation and step usage while preserving retry model and token buckets", async () => { const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "session-1.db")); const stamp = protoNumber(1, 1780000000); diff --git a/packages/provider-core/src/server/usage.ts b/packages/provider-core/src/server/usage.ts index f2cf52e67965..937179707316 100644 --- a/packages/provider-core/src/server/usage.ts +++ b/packages/provider-core/src/server/usage.ts @@ -129,7 +129,8 @@ export interface ProviderUsageInstance { /** One source a `scan` reader read. */ export interface ProviderUsageScan { readonly dir: string; - readonly volumeId: string; + /** Identity of the source across hosts. Defaults to the filesystem identity of `dir`. */ + readonly volumeId?: string; /** Overrides the server's host name in the source fingerprint, for account-wide sources. */ readonly hostId?: string; readonly status?: UsageSource["status"]; diff --git a/packages/provider-opencode/src/server/driver.ts b/packages/provider-opencode/src/server/driver.ts index 3834ec73534b..0573db664969 100644 --- a/packages/provider-opencode/src/server/driver.ts +++ b/packages/provider-opencode/src/server/driver.ts @@ -34,6 +34,7 @@ import * as OpenCode2AdapterV2 from "./v2/adapter.ts"; import type * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter"; import type { ProviderTextGeneration } from "@t3tools/provider-core/server/textGeneration"; import { ProviderDriverError } from "@t3tools/provider-core/server/errors"; +import { openCodeUsageReader, type OpenCodeUsageReaderEnv } from "./usage.ts"; import { readOpenCodeGoUsageLimits } from "./usageLimits.ts"; import { checkOpenCodeProviderStatus, @@ -182,7 +183,11 @@ export type OpenCodeDriverEnv = | OpenCodeRuntime.OpenCodeRuntime | Path.Path; -export const OpenCodeDriver: ProviderDriver = { +export const OpenCodeDriver: ProviderDriver< + OpenCodeSettings, + OpenCodeDriverEnv, + OpenCodeUsageReaderEnv +> = { driverKind: DRIVER_KIND, metadata: { displayName: "OpenCode", @@ -190,6 +195,7 @@ export const OpenCodeDriver: ProviderDriver }, configSchema: OpenCodeSettings, defaultConfig: (): OpenCodeSettings => decodeOpenCodeSettings({}), + usage: openCodeUsageReader, create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; diff --git a/packages/provider-opencode/src/server/usage.test.ts b/packages/provider-opencode/src/server/usage.test.ts new file mode 100644 index 000000000000..8389f85a5f3f --- /dev/null +++ b/packages/provider-opencode/src/server/usage.test.ts @@ -0,0 +1,71 @@ +// @effect-diagnostics nodeBuiltinImport:off - the reader under test opens real +// SQLite databases and walks a real legacy JSON store. +import * as NodeFSP from "node:fs/promises"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; +import * as NodeSqlite from "node:sqlite"; + +import { afterEach, assert, beforeEach, describe, it } from "@effect/vitest"; + +import { readOpenCodeUsage } from "./usage.ts"; + +let dir: string; + +beforeEach(async () => { + dir = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "usage-reader-test-")); +}); + +afterEach(async () => { + await NodeFSP.rm(dir, { recursive: true, force: true }); +}); + +describe("readOpenCodeUsage", () => { + it("counts migrated OpenCode messages once and sees subsequent WAL writes", async () => { + const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "opencode.db")); + try { + db.exec( + "PRAGMA journal_mode = WAL; PRAGMA wal_autocheckpoint = 0; CREATE TABLE message (id TEXT, session_id TEXT, data TEXT)", + ); + const message = { + id: "msg-1", + sessionID: "session-1", + role: "assistant", + modelID: "claude-sonnet-4-5", + time: { created: 1780000000000 }, + cost: 0.25, + tokens: { input: 100, output: 20, reasoning: 5, cache: { read: 30, write: 10 } }, + }; + const insert = db.prepare("INSERT INTO message VALUES (?, ?, ?)"); + insert.run(message.id, message.sessionID, JSON.stringify(message)); + const legacy = NodePath.join(dir, "storage", "message", message.sessionID); + await NodeFSP.mkdir(legacy, { recursive: true }); + await NodeFSP.writeFile(NodePath.join(legacy, "msg-1.json"), JSON.stringify(message)); + const first = await readOpenCodeUsage(dir, 0); + assert.isFalse(first.error); + const records = first.files.flatMap((file) => file.records); + assert.strictEqual(records.length, 1); + assert.deepStrictEqual(records[0]?.totals, { + uncachedInputTokens: 100, + cachedInputTokens: 30, + cacheCreationTokens: 10, + outputTokens: 25, + reasoningTokens: 5, + }); + assert.strictEqual(records[0]?.reportedCostUsd, 0.25); + insert.run( + "msg-2", + message.sessionID, + JSON.stringify({ ...message, id: "msg-2", time: { created: 1780000001000 } }), + ); + const next = await readOpenCodeUsage(dir, 1780000001000); + assert.isFalse(next.error); + assert.deepStrictEqual( + next.files.flatMap((file) => file.records).map((record) => record.dedupeKey), + ["opencode:msg-2"], + ); + assert.isAbove((await NodeFSP.stat(NodePath.join(dir, "opencode.db-wal"))).size, 0); + } finally { + db.close(); + } + }); +}); diff --git a/apps/server/src/usage/opencodeUsageReader.ts b/packages/provider-opencode/src/server/usage.ts similarity index 73% rename from apps/server/src/usage/opencodeUsageReader.ts rename to packages/provider-opencode/src/server/usage.ts index d6a835558ed6..9cc5ce2d0910 100644 --- a/apps/server/src/usage/opencodeUsageReader.ts +++ b/packages/provider-opencode/src/server/usage.ts @@ -1,11 +1,29 @@ // node:sqlite reads live OpenCode databases; Node fs walks legacy JSON history. // @effect-diagnostics nodeBuiltinImport:off +/** + * Usage history for OpenCode, read from its SQLite databases and legacy JSON + * message store under each data directory. + * + * @module provider-opencode/server/usage + */ import * as NodeFSP from "node:fs/promises"; +import * as NodeOS from "node:os"; import * as NodePath from "node:path"; import * as NodeSqlite from "node:sqlite"; import * as NodeTimersPromises from "node:timers/promises"; -import { totalTokens, type UsageRecord } from "@t3tools/provider-core/server/usage"; +import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; +import { + totalTokens, + type ProviderUsageReader, + type UsageRecord, +} from "@t3tools/provider-core/server/usage"; +import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; + +import type { OpenCodeSettings } from "../settings.ts"; function object(value: unknown): Record { return typeof value === "object" && value !== null && !Array.isArray(value) @@ -186,3 +204,55 @@ export async function readOpenCodeUsage( } return { files, missing: !found && !error, error }; } + +/** + * The data directories to read: `OPENCODE_DATA_DIR` (comma-separated) or the + * XDG default, canonicalized so aliases count once. + */ +const resolveOpenCodeDataDirs = Effect.fn("resolveOpenCodeDataDirs")(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const environment = yield* HostProcessEnvironment; + const roots = environment["OPENCODE_DATA_DIR"] + ?.split(",") + .map((value) => value.trim()) + .filter(Boolean); + const dataHome = environment["XDG_DATA_HOME"]?.trim(); + const defaults = [ + path.join( + dataHome && path.isAbsolute(dataHome) + ? dataHome + : path.join(NodeOS.homedir(), ".local", "share"), + "opencode", + ), + ]; + const canonical = new Set(); + for (const root of roots?.length ? roots : defaults) { + const resolved = path.resolve(expandHomePath(root)); + canonical.add(yield* fileSystem.realPath(resolved).pipe(Effect.orElseSucceed(() => resolved))); + } + return [...canonical]; +}); + +export type OpenCodeUsageReaderEnv = FileSystem.FileSystem | Path.Path; + +export const openCodeUsageReader: ProviderUsageReader = { + kind: "scan", + provider: "opencode", + scan: Effect.fn("openCodeUsageReader.scan")(function* ({ windowStartMs }) { + const roots = yield* resolveOpenCodeDataDirs(); + return yield* Effect.forEach( + roots, + (dir) => + Effect.promise(() => readOpenCodeUsage(dir, windowStartMs)).pipe( + Effect.map((result) => ({ + dir, + files: result.missing && !result.error ? null : result.files, + status: result.error ? ("partial" as const) : ("ok" as const), + ...(result.error ? { message: "Some OpenCode history could not be read." } : {}), + })), + ), + { concurrency: "unbounded" }, + ); + }), +}; From 5a9c664b3ec8214cc5847d561efa130ba213eb38 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 15:26:19 -0700 Subject: [PATCH 18/39] refactor(usage): Cursor account usage comes from provider-cursor (#17578) Co-authored-by: Claude Opus 5.5 --- ...viderOrchestrationAdapterInfrastructure.ts | 3 + apps/server/src/provider/builtInDrivers.ts | 5 +- apps/server/src/server.ts | 15 +- apps/server/src/usage/UsageService.test.ts | 58 ++- apps/server/src/usage/UsageService.ts | 285 +-------------- .../src/usage/cursorUsageReader.test.ts | 93 ----- apps/server/src/usage/usagePricing.test.ts | 2 +- .../src/usage/usageTranscriptReader.test.ts | 230 ------------ packages/provider-core/src/server/usage.ts | 3 + packages/provider-cursor/package.json | 16 + packages/provider-cursor/src/server.ts | 6 +- .../src/server/CursorAccountReader.ts | 51 +++ .../src/server/CursorKeychain.test.ts | 54 +++ .../src/server/CursorKeychain.ts | 118 ++++++ .../src/server/CursorUsageAccounts.ts | 342 ++++++++++++++++++ .../src/server/accountCache.test.ts | 2 +- .../src/server/accountCache.ts | 4 +- .../src/server/accountUsage.test.ts | 340 +++++++++++++++++ .../src/server/accountUsage.ts | 48 +-- .../provider-cursor/src/server/driver.test.ts | 6 + packages/provider-cursor/src/server/driver.ts | 30 +- .../src/server/keychainToken.test.ts | 43 --- .../src/server/keychainToken.ts | 55 --- .../src/server/usageLimits.test.ts | 32 +- .../provider-cursor/src/server/usageLimits.ts | 6 +- 25 files changed, 1074 insertions(+), 773 deletions(-) delete mode 100644 apps/server/src/usage/cursorUsageReader.test.ts create mode 100644 packages/provider-cursor/src/server/CursorAccountReader.ts create mode 100644 packages/provider-cursor/src/server/CursorKeychain.test.ts create mode 100644 packages/provider-cursor/src/server/CursorKeychain.ts create mode 100644 packages/provider-cursor/src/server/CursorUsageAccounts.ts rename apps/server/src/usage/cursorAccountCache.test.ts => packages/provider-cursor/src/server/accountCache.test.ts (96%) rename apps/server/src/usage/cursorAccountCache.ts => packages/provider-cursor/src/server/accountCache.ts (98%) create mode 100644 packages/provider-cursor/src/server/accountUsage.test.ts rename apps/server/src/usage/cursorUsageReader.ts => packages/provider-cursor/src/server/accountUsage.ts (90%) delete mode 100644 packages/provider-cursor/src/server/keychainToken.test.ts delete mode 100644 packages/provider-cursor/src/server/keychainToken.ts diff --git a/apps/server/src/provider/ProviderOrchestrationAdapterInfrastructure.ts b/apps/server/src/provider/ProviderOrchestrationAdapterInfrastructure.ts index 65711612b18c..f312b3d58320 100644 --- a/apps/server/src/provider/ProviderOrchestrationAdapterInfrastructure.ts +++ b/apps/server/src/provider/ProviderOrchestrationAdapterInfrastructure.ts @@ -4,6 +4,7 @@ import * as ClaudeAdapterV2 from "../orchestration-v2/Adapters/ClaudeAdapterV2.t import * as CodexAdapterV2 from "../orchestration-v2/Adapters/CodexAdapterV2.ts"; import * as CursorAgentSdk from "@t3tools/provider-cursor/server/CursorAgentSdk"; import * as CursorSdk from "@t3tools/provider-cursor/server/CursorSdk"; +import * as CursorKeychain from "@t3tools/provider-cursor/server/CursorKeychain"; import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; import * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests"; @@ -12,6 +13,7 @@ export type ProviderOrchestrationAdapterInfrastructure = | CodexAdapterV2.CodexAppServerClientFactory | CursorAgentSdk.CursorAgentSdkRunner | CursorSdk.CursorSdk + | CursorKeychain.CursorKeychain | IdAllocator.IdAllocatorV2; /** @@ -24,6 +26,7 @@ export const layer = Layer.mergeAll( ClaudeAdapterV2.layerQueryRunner, CodexAdapterV2.layerAppServerClientFactory, CursorAgentSdk.layer.pipe(Layer.provideMerge(CursorSdk.layer)), + CursorKeychain.layer, IdAllocator.layer, ProviderContinuationRequests.layer, ); diff --git a/apps/server/src/provider/builtInDrivers.ts b/apps/server/src/provider/builtInDrivers.ts index 4576f4ea36bb..9279ef326a25 100644 --- a/apps/server/src/provider/builtInDrivers.ts +++ b/apps/server/src/provider/builtInDrivers.ts @@ -75,7 +75,8 @@ export type BuiltInUsageReadersEnv = | ProviderUsageReaderEnv | ProviderUsageReaderEnv | ProviderUsageReaderEnv - | ProviderUsageReaderEnv; + | ProviderUsageReaderEnv + | ProviderUsageReaderEnv; /** * The drivers that keep usage history, in the order the usage page reads @@ -84,4 +85,4 @@ export type BuiltInUsageReadersEnv = */ export const BUILT_IN_USAGE_DRIVERS: ReadonlyArray< AnyProviderDriver -> = [ClaudeDriver, CodexDriver, GrokDriver, OpenCodeDriver]; +> = [ClaudeDriver, CodexDriver, GrokDriver, OpenCodeDriver, CursorDriver]; diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index a5e23ed65b56..5659b130168d 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -154,7 +154,9 @@ import * as NativeTelemetryClient from "./resourceTelemetry/NativeTelemetryClien import * as ResourceAttribution from "./resourceTelemetry/ResourceAttribution.ts"; import * as ResourceMonitorBinary from "./resourceTelemetry/ResourceMonitorBinary.ts"; import * as ResourceTelemetry from "./resourceTelemetry/ResourceTelemetry.ts"; -import * as CursorUsageReader from "./usage/cursorUsageReader.ts"; +import * as CursorAccountReader from "@t3tools/provider-cursor/server/CursorAccountReader"; +import * as CursorKeychain from "@t3tools/provider-cursor/server/CursorKeychain"; +import * as CursorUsageAccounts from "@t3tools/provider-cursor/server/CursorUsageAccounts"; import * as UsageService from "./usage/UsageService.ts"; import * as RuntimeLayer from "./orchestration-v2/runtimeLayer.ts"; import * as ProjectStore from "./orchestration-v2/ProjectStore.ts"; @@ -252,8 +254,13 @@ const layerBackground = BackgroundPolicy.layer.pipe( ); const layerUsage = UsageService.layer.pipe( + Layer.provide( + CursorUsageAccounts.layer.pipe( + Layer.provide(CursorAccountReader.layer.pipe(Layer.provide(CursorKeychain.layer))), + Layer.provide(ProviderHostLive.layer.pipe(Layer.provide(ServerSecretStore.layer))), + ), + ), Layer.provide(layerServerSettings), - Layer.provide(CursorUsageReader.layer), ); const layerResourceDiagnostics = Layer.mergeAll( @@ -666,9 +673,11 @@ const layerRuntimeCoreDependencies = layerRuntimeCoreDependenciesBase.pipe( const layerRuntimeDependencies = layerRuntimeCoreDependencies.pipe( // Misc. + // Usage reads provider history through the ProviderHost, which needs the + // background policy below it. + Layer.provideMerge(layerUsage), Layer.provideMerge(layerBackground), Layer.provideMerge(layerResourceDiagnostics), - Layer.provideMerge(layerUsage), Layer.provideMerge(TraceDiagnostics.layer), Layer.provideMerge(AnalyticsService.layer), Layer.provideMerge(ExternalLauncher.layer), diff --git a/apps/server/src/usage/UsageService.test.ts b/apps/server/src/usage/UsageService.test.ts index 7e98a5467779..83bace94138a 100644 --- a/apps/server/src/usage/UsageService.test.ts +++ b/apps/server/src/usage/UsageService.test.ts @@ -24,6 +24,7 @@ import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; +import * as Context from "effect/Context"; import * as Layer from "effect/Layer"; import * as Scheduler from "effect/Scheduler"; import * as Schema from "effect/Schema"; @@ -32,7 +33,12 @@ import { HttpClient, HttpClientResponse } from "effect/http"; import * as ServerConfig from "../config.ts"; import * as ServerSettings from "../serverSettings.ts"; -import * as CursorUsageReader from "./cursorUsageReader.ts"; +import * as CursorAccountReader from "@t3tools/provider-cursor/server/CursorAccountReader"; +import * as CursorKeychain from "@t3tools/provider-cursor/server/CursorKeychain"; +import * as CursorUsageAccounts from "@t3tools/provider-cursor/server/CursorUsageAccounts"; +import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; +import * as BackgroundPolicy from "../background/BackgroundPolicy.ts"; +import * as ProviderHostLive from "../provider/ProviderHostLive.ts"; import type { UsageRecord } from "@t3tools/provider-core/server/usage"; import * as UsageService from "./UsageService.ts"; @@ -87,6 +93,27 @@ const setup = Effect.gen(function* () { }; }); +const layerCursorUsageAccounts = ( + reader: Layer.Layer, +) => Layer.fresh(CursorUsageAccounts.layer).pipe(Layer.provide(reader)); + +/** + * A service with its own Cursor account caches over `read`, as a fresh server + * process has. `awaitPersisted` also waits for the account cache writes. + */ +const makeWithCursor = (read: CursorAccountReader.CursorAccountReader["Service"]["read"]) => + Effect.gen(function* () { + const context = yield* Layer.build( + layerCursorUsageAccounts(Layer.succeed(CursorAccountReader.CursorAccountReader, { read })), + ); + const accounts = Context.get(context, CursorUsageAccounts.CursorUsageAccounts); + const service = yield* UsageService.make.pipe(Effect.provideContext(context)); + return { + ...service, + awaitPersisted: Effect.andThen(service.awaitPersisted, accounts.awaitPersisted), + }; + }); + const layerService = (input: { readonly prefix: string; readonly home: string; @@ -97,9 +124,14 @@ const layerService = (input: { readonly environment?: NodeJS.ProcessEnv; readonly platform?: NodeJS.Platform; }) => - ServerConfig.layerTest(process.cwd(), { prefix: input.prefix }).pipe( + layerCursorUsageAccounts( + CursorAccountReader.layer.pipe(Layer.provide(CursorKeychain.layer)), + ).pipe( + Layer.provideMerge(ProviderHostLive.layer), + Layer.provideMerge(Layer.mock(BackgroundPolicy.BackgroundPolicy)({})), + Layer.provideMerge(Layer.mock(ServerSecretStore.ServerSecretStore)({})), + Layer.provideMerge(ServerConfig.layerTest(process.cwd(), { prefix: input.prefix })), Layer.provideMerge(NodeServices.layer), - Layer.provideMerge(CursorUsageReader.layer), Layer.provideMerge(Layer.succeed(HostProcessPlatform, input.platform ?? "linux")), Layer.provideMerge(ServerSettings.layerTest(input.settings)), Layer.provideMerge( @@ -364,9 +396,7 @@ describe("UsageService", () => { const gate = yield* Deferred.make(); cursor.state.gate = gate; yield* Effect.gen(function* () { - const service = yield* UsageService.make.pipe( - Effect.provideService(CursorUsageReader.CursorAccountReader, { read: cursor.read }), - ); + const service = yield* makeWithCursor(cursor.read); // Cold: nothing cached yet, so Cursor answers empty while it refreshes. const cold = yield* service.readSummary(WINDOW); assert.strictEqual(cursorSource(cold)?.refreshing, true); @@ -421,9 +451,7 @@ describe("UsageService", () => { { timestampMs: CURSOR_NOW - HOUR_MS / 2, outputTokens: 7 }, ]; yield* Effect.gen(function* () { - const service = yield* UsageService.make.pipe( - Effect.provideService(CursorUsageReader.CursorAccountReader, { read: cursor.read }), - ); + const service = yield* makeWithCursor(cursor.read); const read = (input: UsageSummaryInput) => service.readSummary({ ...input, awaitRefresh: true }); assert.strictEqual(totalOutputTokens(yield* read(WINDOW)), 19); @@ -470,9 +498,7 @@ describe("UsageService", () => { const cursor = makeFakeCursor(); cursor.state.events = [{ timestampMs: CURSOR_NOW - HOUR_MS * 3, outputTokens: 5 }]; yield* Effect.gen(function* () { - const service = yield* UsageService.make.pipe( - Effect.provideService(CursorUsageReader.CursorAccountReader, { read: cursor.read }), - ); + const service = yield* makeWithCursor(cursor.read); yield* service.readSummary({ ...WINDOW, awaitRefresh: true }); yield* TestClock.adjust(Duration.minutes(2)); @@ -513,17 +539,13 @@ describe("UsageService", () => { { timestampMs: CURSOR_NOW - 3 * HOUR_MS, outputTokens: 7 }, ]; yield* Effect.gen(function* () { - const first = yield* UsageService.make.pipe( - Effect.provideService(CursorUsageReader.CursorAccountReader, { read: before.read }), - ); + const first = yield* makeWithCursor(before.read); const original = yield* first.readSummary({ ...WINDOW, awaitRefresh: true }); yield* first.awaitPersisted; const after = makeFakeCursor(); after.state.events = before.state.events; - const restarted = yield* UsageService.make.pipe( - Effect.provideService(CursorUsageReader.CursorAccountReader, { read: after.read }), - ); + const restarted = yield* makeWithCursor(after.read); const restored = yield* restarted.readSummary(WINDOW); assert.isUndefined(cursorSource(restored)?.refreshing); assert.deepStrictEqual(restored.buckets, original.buckets); diff --git a/apps/server/src/usage/UsageService.ts b/apps/server/src/usage/UsageService.ts index c8a1891ce8f8..2f48fc5abcab 100644 --- a/apps/server/src/usage/UsageService.ts +++ b/apps/server/src/usage/UsageService.ts @@ -12,9 +12,9 @@ * remain visible. Antigravity databases are memoised in memory while the * database and its WAL keep the same `(size, mtime, ctime)`. * - * Cursor's account API is slow, so its source answers from a cache and marks - * itself `refreshing` while a background refresh runs; `awaitRefresh` waits - * for that refresh instead. See `cursorAccountCache`. + * A scan reader with a slow source (an account API) may answer from its own + * cache and mark itself `refreshing` while a background refresh runs; + * `awaitRefresh` waits for that refresh instead. * * @module UsageService */ @@ -32,7 +32,7 @@ import { type UsageSummaryInput, UsageReadError, } from "@t3tools/contracts"; -import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; import * as Cause from "effect/Cause"; import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; @@ -64,18 +64,6 @@ import type { UsageRecord, } from "@t3tools/provider-core/server/usage"; import { makeAntigravityUsageCache, readAntigravityUsage } from "./antigravityUsageReader.ts"; -import { - CURSOR_ACCOUNT_CACHE_FILE_NAME, - CURSOR_ACCOUNT_TTL_MS, - cursorFetchRange, - decodeCursorAccountCaches, - encodeCursorAccountCaches, - isCursorCacheFresh, - mergeCursorFetch, - type CursorAccountCache, - type CursorCredentialSource, -} from "./cursorAccountCache.ts"; -import * as CursorUsageReader from "./cursorUsageReader.ts"; import { resolveModelAliases, UsageAggregator } from "./usageAggregation.ts"; import { createOverrideRateTable, parseRateTable, type RateTable } from "./usagePricing.ts"; import { @@ -116,8 +104,6 @@ const MAX_HOURLY_WINDOW_MS = 24 * 60 * 60 * 1000; */ const CACHE_RETENTION_DAYS = 92; -const CURSOR_ACCOUNT_READ_ERROR = "Cursor account usage could not be read."; - /** Transcripts parsed at once. More gains little once the disk stays busy. */ const TRANSCRIPT_READ_CONCURRENCY = 4; @@ -248,8 +234,6 @@ export const make = Effect.gen(function* () { const settingsService = yield* ServerSettings.ServerSettingsService; const httpClient = yield* HttpClient.HttpClient; const hostEnvironment = yield* HostProcessEnvironment; - const platform = yield* HostProcessPlatform; - const cursorAccountReader = yield* CursorUsageReader.CursorAccountReader; // The readers yield their own services; scans run them against this context. const readerContext = yield* Effect.context(); @@ -257,20 +241,6 @@ export const make = Effect.gen(function* () { const antigravityCache = makeAntigravityUsageCache(); const sourceCache = new Map(); let cacheDirty = false; - /** Cursor account caches by credential source. */ - const cursorCaches = new Map(); - let cursorCacheDirty = false; - /** - * The last failed refresh per credential source, standing for a TTL so a - * client refetching a broken login does not refetch Cursor each time. A - * `null` error means there is no login: no source to report. - */ - const cursorFailures = new Map< - string, - { readonly atMs: number; readonly error: string | null } - >(); - /** The refresh in flight per credential source, which every read joins. */ - const cursorRefreshes = new Map>(); const isWithinDirectory = (filePath: string, dir: string) => { const relative = path.relative(dir, filePath); return relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative); @@ -279,7 +249,6 @@ export const make = Effect.gen(function* () { const ratesCachePath = path.join(config.stateDir, "usage-model-rates.json"); const scanCachePath = path.join(config.stateDir, SCAN_CACHE_FILE_NAME); const legacyScanCachePath = path.join(config.stateDir, LEGACY_SCAN_CACHE_FILE_NAME); - const cursorCachePath = path.join(config.stateDir, CURSOR_ACCOUNT_CACHE_FILE_NAME); const writeCacheFile = (filePath: string, contents: string) => writeFileStringAtomically({ filePath, contents }).pipe( Effect.provideService(FileSystem.FileSystem, fileSystem), @@ -453,7 +422,7 @@ export const make = Effect.gen(function* () { }); /** - * Loads the persisted scan and Cursor account caches exactly once per process. + * Loads the persisted scan cache exactly once per process. * * `Effect.cached` makes concurrent first readers await the same load rather * than each seeing a "loaded" flag set before the read finished and cold @@ -466,9 +435,6 @@ export const make = Effect.gen(function* () { Effect.flatMap((raw) => decodeScanCacheFile(raw)), Effect.catchCause(() => Effect.succeed(null)), ); - for (const [key, cache] of decodeCursorAccountCaches(yield* readDocument(cursorCachePath))) { - cursorCaches.set(key, cache); - } let document = yield* readDocument(scanCachePath); if (document === null) { document = yield* readDocument(legacyScanCachePath); @@ -492,7 +458,7 @@ export const make = Effect.gen(function* () { // keeps an older snapshot from landing after a newer one. const persistLock = yield* Semaphore.make(1); - // Each dirty flag is cleared before encoding, so a change while the write is + // The dirty flag is cleared before encoding, so a change while the write is // in flight marks it dirty again. A failed write restores the flag, so the // next persist retries instead of leaving disk stale. A cache we cannot // write is a slower next start, not a failed read. @@ -510,17 +476,6 @@ export const make = Effect.gen(function* () { ), ); } - if (cursorCacheDirty) { - cursorCacheDirty = false; - yield* Effect.sync(() => encodeCursorAccountCaches(cursorCaches)).pipe( - Effect.flatMap((contents) => writeCacheFile(cursorCachePath, contents)), - Effect.catchCause(() => - Effect.sync(() => { - cursorCacheDirty = true; - }), - ), - ); - } }).pipe(persistLock.withPermit, Effect.withSpan("UsageService.persistCaches")); const pendingPersists = new Set>(); @@ -674,162 +629,6 @@ export const make = Effect.gen(function* () { return { provider, dir, volumeId, files: parsedFiles } satisfies ScannedDir; }); - /** Fetches what one account cache is missing, then persists it if anything changed. */ - const refreshCursorAccount = Effect.fn("UsageService.refreshCursorAccount")(function* ( - credential: CursorCredentialSource, - credentialKey: string, - retentionStartMs: number, - ) { - const nowMs = yield* Clock.currentTimeMillis; - const fetchMissing = (cache: CursorAccountCache | undefined) => { - const range = cursorFetchRange(cache, retentionStartMs, nowMs); - return cursorAccountReader - .read(credential, range.sinceMs, range.untilMs) - .pipe(Effect.map((result) => ({ range, result }))); - }; - let base = cursorCaches.get(credentialKey); - let fetched = yield* fetchMissing(base); - // Another login's history replaces the cached account's, even if reading it fails. - if ( - base !== undefined && - fetched.result.accountKey !== null && - fetched.result.accountKey !== base.accountKey - ) { - cursorCaches.delete(credentialKey); - cursorCacheDirty = true; - base = undefined; - fetched = yield* fetchMissing(base); - } - const { range, result } = fetched; - if (result.missing || result.error !== null || result.accountKey === null) { - cursorFailures.set(credentialKey, { - atMs: nowMs, - error: result.missing || result.error !== null ? result.error : CURSOR_ACCOUNT_READ_ERROR, - }); - yield* schedulePersist; - return; - } - const merged = mergeCursorFetch( - base, - result.accountKey, - range, - result.records, - nowMs, - retentionStartMs, - ); - cursorCaches.set(credentialKey, merged.cache); - cursorFailures.delete(credentialKey); - // An unchanged edge is not worth rewriting the file for: after a restart - // the cache just refetches a slightly wider edge. - if (merged.changed) { - cursorCacheDirty = true; - yield* schedulePersist; - } - }); - - /** Joins the refresh in flight, or starts one. */ - const startCursorRefresh = ( - credential: CursorCredentialSource, - credentialKey: string, - retentionStartMs: number, - ) => - // Enrollment and fork are atomic, so an interrupted caller cannot leave a - // registered refresh that nothing will finish. - Effect.uninterruptible( - Effect.gen(function* () { - const current = cursorRefreshes.get(credentialKey); - if (current !== undefined) return current; - const done = Deferred.makeUnsafe(); - cursorRefreshes.set(credentialKey, done); - // Detached: a departing client must not cancel a fetch later reads reuse. - yield* refreshCursorAccount(credential, credentialKey, retentionStartMs).pipe( - Effect.catchCause(() => - Clock.currentTimeMillis.pipe( - Effect.map((atMs) => - cursorFailures.set(credentialKey, { atMs, error: CURSOR_ACCOUNT_READ_ERROR }), - ), - ), - ), - Effect.ensuring( - Effect.suspend(() => { - cursorRefreshes.delete(credentialKey); - return Deferred.succeed(done, undefined); - }), - ), - Effect.forkDetach, - ); - return done; - }), - ); - - /** - * The Cursor account source. A cache inside its TTL, or a refresh that failed - * inside it, answers directly. Otherwise a refresh starts: `awaitRefresh` - * waits for it, and anything else answers from the cache marked `refreshing`. - */ - const cursorAccountSource = Effect.fn("UsageService.cursorAccountSource")(function* ( - credential: CursorCredentialSource, - authPath: string, - windowStartMs: number, - retentionStartMs: number, - awaitRefresh: boolean, - ) { - // No saved login means there is no account source to report, not a setup error. - if ( - typeof credential === "string" && - !(yield* fileSystem.exists(credential).pipe(Effect.orElseSucceed(() => true))) - ) { - return null; - } - const credentialKey = typeof credential === "string" ? credential : "keychain"; - const nowMs = yield* Clock.currentTimeMillis; - const recentFailure = cursorFailures.get(credentialKey); - let refreshing = false; - if ( - (recentFailure === undefined || nowMs - recentFailure.atMs >= CURSOR_ACCOUNT_TTL_MS) && - !isCursorCacheFresh(cursorCaches.get(credentialKey), nowMs) - ) { - const refresh = yield* startCursorRefresh(credential, credentialKey, retentionStartMs); - if (awaitRefresh) yield* Deferred.await(refresh); - else refreshing = true; - } - - const cache = cursorCaches.get(credentialKey); - const failure = refreshing ? undefined : cursorFailures.get(credentialKey); - const failureMessage = failure === undefined ? undefined : failure.error; - if (failureMessage === null) return null; - if (cache === undefined) { - return { - provider: "cursor", - dir: authPath, - volumeId: yield* Effect.promise(() => readDirectoryVolumeId(authPath)), - // Never combine a local fallback with another server's account-wide history. - ...(refreshing - ? { files: [], refreshing: true } - : { files: null, message: failureMessage ?? CURSOR_ACCOUNT_READ_ERROR }), - } satisfies ScannedDir; - } - // The same account includes CLI and desktop history from every machine. - // A stable remote fingerprint prevents connected environments counting it twice. - const source = `cursor-account:${cache.accountKey}`; - return { - provider: "cursor", - dir: source, - hostId: "cursor.com", - volumeId: cache.accountKey, - files: [ - { - path: source, - records: cache.records.filter((record) => record.timestampMs >= windowStartMs), - }, - ], - ...(failureMessage === undefined - ? { status: "ok" } - : { status: "partial", message: failureMessage }), - ...(refreshing ? { refreshing: true } : {}), - } satisfies ScannedDir; - }); - const collectDirs = Effect.fn("UsageService.collectDirs")(function* ( windowStartMs: number, settings: ServerSettingsValue, @@ -862,6 +661,7 @@ export const make = Effect.gen(function* () { reader .scan({ instances: usageInstances(driver, settings), + settings, windowStartMs, retentionCutoffMs, awaitRefresh, @@ -884,7 +684,7 @@ export const make = Effect.gen(function* () { Effect.provideContext(readerContext), ), { concurrency: "unbounded" }, - ).pipe(Effect.map((sources) => sources.flat())); + ); const antigravity = Effect.gen(function* () { const antigravityRoots = yield* envRoots("ANTIGRAVITY_DATA_DIR", [ @@ -949,78 +749,19 @@ export const make = Effect.gen(function* () { return scanned; }); - const cursor = Effect.gen(function* () { - const cursorUserHome = - (platform === "win32" ? hostEnvironment["USERPROFILE"] : hostEnvironment["HOME"]) || home; - const configHome = hostEnvironment["XDG_CONFIG_HOME"]?.trim(); - const cursorHome = - platform === "darwin" - ? path.join(cursorUserHome, "Library", "Application Support") - : platform === "win32" - ? hostEnvironment["APPDATA"] || path.join(cursorUserHome, "AppData", "Roaming") - : configHome && path.isAbsolute(configHome) - ? configHome - : path.join(cursorUserHome, ".config"); - const cursorAuthPath = - platform === "darwin" - ? path.join(cursorUserHome, ".cursor", "auth.json") - : path.join(cursorHome, platform === "win32" ? "Cursor" : "cursor", "auth.json"); - const credentialStore = hostEnvironment["AGENT_CLI_CREDENTIAL_STORE"]; - const loginUnavailable = - Boolean(hostEnvironment["CURSOR_AUTH_TOKEN"]?.trim()) || - Boolean(hostEnvironment["CURSOR_API_KEY"]?.trim()) || - credentialStore === "memory"; - const useKeychain = platform === "darwin" && credentialStore !== "file"; - if (useKeychain && !loginUnavailable && !settings.cursorKeychainUsageEnabled) { - return [ - { - provider: "cursor", - dir: cursorAuthPath, - volumeId: "", - files: null, - message: "Cursor account usage is off on this environment.", - action: "enableCursorKeychain", - } satisfies ScannedDir, - ]; - } - if (loginUnavailable) { - return [ - { - provider: "cursor", - dir: cursorAuthPath, - volumeId: yield* Effect.promise(() => readDirectoryVolumeId(cursorAuthPath)), - files: null, - message: "Cursor account history needs a Cursor CLI login on this server.", - } satisfies ScannedDir, - ]; - } - const source = yield* cursorAccountSource( - useKeychain ? { kind: "keychain" } : cursorAuthPath, - cursorAuthPath, - windowStartMs, - retentionCutoffMs, - awaitRefresh, - ); - return source === null ? [] : [source]; - }); - // Independent sources scan together. Transcript directories go one at a // time, so open files stay at `TRANSCRIPT_READ_CONCURRENCY`. The result // keeps this order, since aggregation keeps the first copy of a duplicate. - const [transcripts, scanDirs, antigravityDirs, cursorDirs] = yield* Effect.all( - [ - Effect.forEach(dirs, (dir) => scanTranscriptDir(dir, windowStartMs)), - scans, - antigravity, - cursor, - ], + const [transcripts, scanDirs, antigravityDirs] = yield* Effect.all( + [Effect.forEach(dirs, (dir) => scanTranscriptDir(dir, windowStartMs)), scans, antigravity], { concurrency: "unbounded" }, ); + // Antigravity reads between the other scan readers until it has a reader of its own. const scanned: readonly ScannedDir[] = [ ...transcripts, - ...scanDirs, + ...scanDirs.slice(0, -1).flat(), ...antigravityDirs, - ...cursorDirs, + ...(scanDirs.at(-1) ?? []), ]; return scanned; }); diff --git a/apps/server/src/usage/cursorUsageReader.test.ts b/apps/server/src/usage/cursorUsageReader.test.ts deleted file mode 100644 index 4897f4604a9e..000000000000 --- a/apps/server/src/usage/cursorUsageReader.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { assert, describe, it } from "@effect/vitest"; - -import { CursorKeychainTimeoutError } from "@t3tools/provider-cursor/server"; -import { readCursorAccountUsage } from "./cursorUsageReader.ts"; - -describe("readCursorAccountUsage", () => { - it("asks for Keychain approval when the prompt goes unanswered", async () => { - const result = await readCursorAccountUsage( - { kind: "keychain" }, - 0, - 1, - () => Promise.reject(new Error("no network expected")), - () => Promise.reject(new CursorKeychainTimeoutError()), - ); - assert.deepStrictEqual(result, { - accountKey: null, - records: [], - missing: false, - error: "Allow Keychain access on the Mac running T3 Code, then refresh.", - }); - }); - - it("reads the pages behind the first together and keeps them in page order", async () => { - const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; - let inFlight = 0; - let mostInFlight = 0; - const result = await readCursorAccountUsage( - { kind: "keychain" }, - 0, - 1781000000000, - async (_url, init) => { - const page: number = JSON.parse(String(init.body)).page; - inFlight++; - mostInFlight = Math.max(mostInFlight, inFlight); - // Later pages answer first, so record order cannot come from arrival order. - for (let turn = page; turn < 9; turn++) await Promise.resolve(); - inFlight--; - return Response.json({ - totalUsageEventsCount: 8001, - usageEventsDisplay: Array.from({ length: page === 9 ? 1 : 1000 }, (_, index) => ({ - timestamp: String(1780000000000 + (page - 1) * 1000 + index), - model: "gpt-5", - tokenUsage: { inputTokens: 1 }, - })), - }); - }, - async () => accessToken, - ); - assert.isNull(result.error); - assert.strictEqual(mostInFlight, 6); - assert.deepStrictEqual( - result.records.map((record) => record.timestampMs), - Array.from({ length: 8001 }, (_, index) => 1780000000000 + index), - ); - }); - - it("ends like a page-by-page read when a page fails, leaving no request open", async () => { - const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; - const fullPage = { - totalUsageEventsCount: 8001, - usageEventsDisplay: Array.from({ length: 1000 }, () => ({ tokenUsage: null })), - }; - // Page 3 fails while the pages after it are still waiting on Cursor. - for (const [secondPage, error] of [ - [() => Response.json(fullPage), "Cursor account usage could not be read."], - [() => new Response(null, { status: 401 }), "Sign in to Cursor again to read account usage."], - ] as const) { - let open = 0; - const result = await readCursorAccountUsage( - { kind: "keychain" }, - 0, - 1781000000000, - (_url, init) => { - const page: number = JSON.parse(String(init.body)).page; - if (page === 1) return Promise.resolve(Response.json(fullPage)); - if (page === 2) return Promise.resolve(secondPage()); - if (page === 3) return Promise.reject(new Error("connection reset")); - open++; - return new Promise((_resolve, reject) => { - init.signal?.addEventListener("abort", () => { - open--; - reject(init.signal?.reason); - }); - }); - }, - async () => accessToken, - ); - assert.strictEqual(result.error, error); - assert.deepStrictEqual(result.records, []); - assert.strictEqual(open, 0); - } - }); -}); diff --git a/apps/server/src/usage/usagePricing.test.ts b/apps/server/src/usage/usagePricing.test.ts index 3afa9ff90824..b13a06a1c2d0 100644 --- a/apps/server/src/usage/usagePricing.test.ts +++ b/apps/server/src/usage/usagePricing.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "@effect/vitest"; -import { cursorRateModel } from "./cursorUsageReader.ts"; +import { cursorRateModel } from "@t3tools/provider-cursor/server/accountUsage"; import type { UsageSpeed } from "@t3tools/provider-core/server/usage"; import { cacheSavingsUsd, diff --git a/apps/server/src/usage/usageTranscriptReader.test.ts b/apps/server/src/usage/usageTranscriptReader.test.ts index 60fdc8f27367..2cac91423697 100644 --- a/apps/server/src/usage/usageTranscriptReader.test.ts +++ b/apps/server/src/usage/usageTranscriptReader.test.ts @@ -10,7 +10,6 @@ import { afterEach, assert, beforeEach, describe, it } from "@effect/vitest"; import { TEST_FORMATS } from "./usageTestFormats.ts"; import { readTranscriptRecords } from "./usageTranscriptReader.ts"; -import { readCursorAccountUsage } from "./cursorUsageReader.ts"; import { makeAntigravityUsageCache, readAntigravityUsage } from "./antigravityUsageReader.ts"; let dir: string; @@ -252,235 +251,6 @@ describe("readTranscriptRecords resume", () => { }); describe("SQLite usage readers", () => { - it("reads Cursor account history with the default macOS Keychain login", async () => { - const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; - let keychainReads = 0; - const result = await readCursorAccountUsage( - { kind: "keychain" }, - 0, - 1781000000000, - async (_url, init) => { - assert.include(new Headers(init.headers).get("cookie") ?? "", "demo%3A%3A"); - return Response.json({ totalUsageEventsCount: 0, usageEventsDisplay: [] }); - }, - async () => { - keychainReads++; - return accessToken; - }, - ); - assert.strictEqual(keychainReads, 1); - assert.isNull(result.error); - assert.isFalse(result.missing); - assert.isNotNull(result.accountKey); - }); - - it("reads paginated Cursor account history including headless calls with separate cache tokens", async () => { - const authPath = NodePath.join(dir, "auth.json"); - const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo", exp: 4102444800 })).toString("base64url")}.signature`; - await NodeFSP.writeFile(authPath, JSON.stringify({ accessToken })); - const pages: number[] = []; - const signals: AbortSignal[] = []; - const request = async (url: string, init: RequestInit) => { - assert.strictEqual(String(url), "https://cursor.com/api/dashboard/get-filtered-usage-events"); - assert.strictEqual(init?.redirect, "error"); - const headers = new Headers(init?.headers); - assert.strictEqual(headers.get("origin"), "https://cursor.com"); - assert.include(headers.get("cookie") ?? "", "WorkosCursorSessionToken=demo%3A%3A"); - const body = JSON.parse(String(init?.body)); - pages.push(body.page); - if (init.signal) signals.push(init.signal); - return Response.json({ - totalUsageEventsCount: 1001, - usageEventsDisplay: Array.from({ length: body.page === 1 ? 1000 : 1 }, (_, index) => ({ - timestamp: String(1780000000000 + ((body.page - 1) * 1000 + index) * 1000), - model: "claude-sonnet-4-5", - conversationId: `conversation-${body.page}`, - isHeadless: body.page === 2, - chargedCents: 0, - tokenUsage: { - inputTokens: 10, - outputTokens: 5, - cacheReadTokens: 30, - cacheWriteTokens: 2, - totalCents: 25, - }, - })), - }); - }; - const result = await readCursorAccountUsage(authPath, 0, 1781000000000, request); - assert.isNull(result.error); - assert.deepStrictEqual(pages, [1, 2]); - assert.lengthOf(signals, 2); - assert.notStrictEqual(signals[0], signals[1]); - assert.strictEqual(result.records.length, 1001); - assert.strictEqual(result.records.at(-1)?.sessionId, "conversation-2"); - assert.deepStrictEqual(result.records[0]?.totals, { - uncachedInputTokens: 10, - cachedInputTokens: 30, - cacheCreationTokens: 2, - outputTokens: 5, - reasoningTokens: 0, - }); - assert.strictEqual(result.records[0]?.reportedCostUsd, 0.25); - assert.isFalse(result.accountKey?.includes("demo") ?? true); - }); - - it("reads Cursor account history beyond 100 pages", async () => { - const authPath = NodePath.join(dir, "auth.json"); - const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; - await NodeFSP.writeFile(authPath, JSON.stringify({ accessToken })); - const fullPage = Array.from({ length: 1000 }, () => ({ tokenUsage: null })); - let requests = 0; - const result = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => { - requests += 1; - return Response.json({ - totalUsageEventsCount: 100_001, - usageEventsDisplay: requests <= 100 ? fullPage : [{ tokenUsage: null }], - }); - }); - assert.isNull(result.error); - assert.strictEqual(requests, 101); - assert.deepStrictEqual(result.records, []); - }); - - it("accepts confirmed empty Cursor usage but rejects error envelopes", async () => { - const authPath = NodePath.join(dir, "auth.json"); - const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; - await NodeFSP.writeFile(authPath, JSON.stringify({ accessToken })); - for (const body of [ - {}, - { totalUsageEventsCount: 0 }, - { totalUsageEventsCount: 0, usageEventsDisplay: [] }, - ]) { - const result = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => - Response.json(body), - ); - assert.isNull(result.error); - assert.deepStrictEqual(result.records, []); - assert.isFalse(result.missing); - } - for (const body of [ - { error: "upstream error" }, - { detail: "unknown error envelope" }, - { totalUsageEventsCount: 0, error: "upstream error" }, - null, - [], - "invalid", - 0, - ]) { - const result = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => - Response.json(body), - ); - assert.isNotNull(result.error); - assert.deepStrictEqual(result.records, []); - } - }); - - it("requires a terminal Cursor page after a full page reaches the reported count", async () => { - const authPath = NodePath.join(dir, "auth.json"); - const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; - await NodeFSP.writeFile(authPath, JSON.stringify({ accessToken })); - let requests = 0; - const result = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => { - requests++; - return Response.json( - requests === 1 - ? { - totalUsageEventsCount: 1000, - usageEventsDisplay: Array.from({ length: 1000 }, (_, index) => ({ - timestamp: String(1780000000000 + index), - model: "gpt-5", - tokenUsage: { inputTokens: 10, outputTokens: 5 }, - })), - } - : { totalUsageEventsCount: 1000 }, - ); - }); - assert.isNull(result.error); - assert.strictEqual(result.records.length, 1000); - assert.strictEqual(requests, 2); - }); - - it("removes only count-proven Cursor boundary copies and preserves identical billed events", async () => { - const authPath = NodePath.join(dir, "auth.json"); - const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; - await NodeFSP.writeFile(authPath, JSON.stringify({ accessToken })); - const event = (index: number) => ({ - timestamp: String(1780000000000 + index), - model: "gpt-5", - tokenUsage: { inputTokens: 10, outputTokens: 5, totalCents: 1 }, - }); - for (const total of [2000, 2001]) { - let requests = 0; - const result = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => { - requests++; - return Response.json({ - totalUsageEventsCount: total, - usageEventsDisplay: - requests === 1 - ? Array.from({ length: 1000 }, (_, index) => event(index)) - : requests === 2 - ? Array.from({ length: 1000 }, (_, index) => event(999 + index)) - : [event(1999)], - }); - }); - assert.isNull(result.error); - assert.strictEqual(result.records.length, total); - assert.strictEqual(requests, 3); - assert.strictEqual(result.records.at(-1)?.timestampMs, 1780000001999); - assert.strictEqual( - result.records.filter((record) => record.timestampMs === 1780000000999).length, - total === 2000 ? 1 : 2, - ); - assert.strictEqual(new Set(result.records.map((record) => record.dedupeKey)).size, total); - } - let requests = 0; - const inconsistent = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => { - requests++; - return Response.json({ - totalUsageEventsCount: 1001, - usageEventsDisplay: - requests === 1 - ? Array.from({ length: 1000 }, (_, index) => event(index)) - : [event(500), event(1000)], - }); - }); - assert.isNotNull(inconsistent.error); - assert.deepStrictEqual(inconsistent.records, []); - }); - - it("does not present truncated Cursor account pages or authentication failures as complete history", async () => { - const authPath = NodePath.join(dir, "auth.json"); - const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo", exp: 4102444800 })).toString("base64url")}.signature`; - await NodeFSP.writeFile(authPath, JSON.stringify({ accessToken })); - const truncated = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => - Response.json({ totalUsageEventsCount: 101, usageEventsDisplay: [] }), - ); - assert.isNotNull(truncated.error); - assert.deepStrictEqual(truncated.records, []); - const denied = await readCursorAccountUsage( - authPath, - 0, - 1781000000000, - async () => new Response(accessToken, { status: 401 }), - ); - assert.isNotNull(denied.error); - assert.isFalse(denied.error?.includes(accessToken) ?? true); - assert.deepStrictEqual(denied.records, []); - let requested = false; - const missing = await readCursorAccountUsage( - NodePath.join(dir, "missing.json"), - 0, - 1781000000000, - async () => { - requested = true; - return Response.json({}); - }, - ); - assert.isTrue(missing.missing); - assert.isFalse(requested); - }); - it("deduplicates Antigravity generation and step usage while preserving retry model and token buckets", async () => { const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "session-1.db")); const stamp = protoNumber(1, 1780000000); diff --git a/packages/provider-core/src/server/usage.ts b/packages/provider-core/src/server/usage.ts index 937179707316..62ecb5796b1b 100644 --- a/packages/provider-core/src/server/usage.ts +++ b/packages/provider-core/src/server/usage.ts @@ -11,6 +11,7 @@ */ import type { ProviderInstanceId, + ServerSettings, UsageProviderKind, UsageSource, UsageTokenTotals, @@ -146,6 +147,8 @@ export interface ProviderUsageScan { export interface ProviderUsageScanInput { readonly instances: ReadonlyArray>; + /** The settings snapshot the whole read runs against. */ + readonly settings: ServerSettings; /** Files last written before this cannot hold records inside the window. */ readonly windowStartMs: number; /** The oldest usage the server keeps; account caches cover back to here. */ diff --git a/packages/provider-cursor/package.json b/packages/provider-cursor/package.json index e7d2a7c8b353..b139d6ecc8dd 100644 --- a/packages/provider-cursor/package.json +++ b/packages/provider-cursor/package.json @@ -23,6 +23,22 @@ "types": "./src/server/CursorAgentSdk.ts", "import": "./src/server/CursorAgentSdk.ts" }, + "./server/accountUsage": { + "types": "./src/server/accountUsage.ts", + "import": "./src/server/accountUsage.ts" + }, + "./server/CursorAccountReader": { + "types": "./src/server/CursorAccountReader.ts", + "import": "./src/server/CursorAccountReader.ts" + }, + "./server/CursorKeychain": { + "types": "./src/server/CursorKeychain.ts", + "import": "./src/server/CursorKeychain.ts" + }, + "./server/CursorUsageAccounts": { + "types": "./src/server/CursorUsageAccounts.ts", + "import": "./src/server/CursorUsageAccounts.ts" + }, "./server/CursorSdk": { "types": "./src/server/CursorSdk.ts", "import": "./src/server/CursorSdk.ts" diff --git a/packages/provider-cursor/src/server.ts b/packages/provider-cursor/src/server.ts index 471737aa58fd..cc56dd7978e1 100644 --- a/packages/provider-cursor/src/server.ts +++ b/packages/provider-cursor/src/server.ts @@ -1,10 +1,8 @@ /** - * Cursor's server entry: the driver the server registers, its adapter - * driver, the SDK runner layer the server provides once, and the keychain - * token reader the usage scanner shares. + * Cursor's server entry: the driver the server registers and its adapter + * driver. * * @module provider-cursor/server */ export { CursorDriver, type CursorDriverEnv } from "./server/driver.ts"; export { CursorAdapterV2Driver, type CursorAdapterV2DriverEnv } from "./server/adapter.ts"; -export { CursorKeychainTimeoutError, readMacCursorAccessToken } from "./server/keychainToken.ts"; diff --git a/packages/provider-cursor/src/server/CursorAccountReader.ts b/packages/provider-cursor/src/server/CursorAccountReader.ts new file mode 100644 index 000000000000..a6922ef87a53 --- /dev/null +++ b/packages/provider-cursor/src/server/CursorAccountReader.ts @@ -0,0 +1,51 @@ +/** + * CursorAccountReader - reads one range of Cursor account usage from its + * dashboard API. A service so tests can stand in for Cursor's API. + * + * @module provider-cursor/server/CursorAccountReader + */ +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Result from "effect/Result"; + +import type { CursorCredentialSource } from "./accountCache.ts"; +import { readCursorAccountUsage, type CursorAccountUsageReadResult } from "./accountUsage.ts"; +import * as CursorKeychain from "./CursorKeychain.ts"; + +export class CursorAccountReader extends Context.Service< + CursorAccountReader, + { + readonly read: ( + credentialSource: CursorCredentialSource, + sinceMs: number, + untilMs: number, + ) => Effect.Effect; + } +>()("@t3tools/provider-cursor/server/CursorAccountReader") {} + +/** Reads Cursor's dashboard API with the saved CLI or Keychain login. */ +export const layer = Layer.effect( + CursorAccountReader, + Effect.gen(function* () { + const keychain = yield* CursorKeychain.CursorKeychain; + return CursorAccountReader.of({ + read: (credentialSource, sinceMs, untilMs) => + Effect.gen(function* () { + // Only a Keychain login asks the Keychain. The dashboard reader + // turns its outcome, a failure included, into the source's message. + const token = + typeof credentialSource === "string" + ? Result.succeed(null) + : yield* Effect.result(keychain.accessToken); + return yield* Effect.promise(() => + readCursorAccountUsage(credentialSource, sinceMs, untilMs, globalThis.fetch, () => + Result.isSuccess(token) + ? Promise.resolve(token.success) + : Promise.reject(token.failure), + ), + ); + }), + }); + }), +); diff --git a/packages/provider-cursor/src/server/CursorKeychain.test.ts b/packages/provider-cursor/src/server/CursorKeychain.test.ts new file mode 100644 index 000000000000..ebdcdbd15fe5 --- /dev/null +++ b/packages/provider-cursor/src/server/CursorKeychain.test.ts @@ -0,0 +1,54 @@ +import { assert, describe, it } from "@effect/vitest"; +import * as Deferred from "effect/Deferred"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Fiber from "effect/Fiber"; +import * as TestClock from "effect/testing/TestClock"; + +import * as CursorKeychain from "./CursorKeychain.ts"; + +describe("CursorKeychain", () => { + it.effect("shares concurrent reads and rechecks after the cache expires", () => + Effect.gen(function* () { + let reads = 0; + const keychain = CursorKeychain.make( + Effect.sync(() => { + reads++; + return `token-${reads}`; + }), + ); + assert.deepStrictEqual( + yield* Effect.all([keychain.accessToken, keychain.accessToken], { + concurrency: "unbounded", + }), + ["token-1", "token-1"], + ); + assert.strictEqual(yield* keychain.accessToken, "token-1"); + assert.strictEqual(reads, 1); + yield* TestClock.adjust(Duration.minutes(5)); + assert.strictEqual(yield* keychain.accessToken, "token-2"); + }), + ); + + it.effect("gives up on an unanswered prompt and reuses it on the next read", () => + Effect.gen(function* () { + let reads = 0; + const allow = yield* Deferred.make(); + const keychain = CursorKeychain.make( + Effect.suspend(() => { + reads++; + return Deferred.await(allow); + }), + Duration.millis(1), + ); + const timedOut = yield* keychain.accessToken.pipe(Effect.forkChild); + yield* TestClock.adjust(Duration.millis(1)); + const error = yield* Fiber.join(timedOut).pipe(Effect.flip); + assert.isTrue(CursorKeychain.isCursorKeychainTimeoutError(error)); + const retry = yield* keychain.accessToken.pipe(Effect.forkChild); + yield* Deferred.succeed(allow, "token"); + assert.strictEqual(yield* Fiber.join(retry), "token"); + assert.strictEqual(reads, 1); + }), + ); +}); diff --git a/packages/provider-cursor/src/server/CursorKeychain.ts b/packages/provider-cursor/src/server/CursorKeychain.ts new file mode 100644 index 000000000000..30beb6703434 --- /dev/null +++ b/packages/provider-cursor/src/server/CursorKeychain.ts @@ -0,0 +1,118 @@ +/** + * CursorKeychain - the Cursor CLI's macOS Keychain login, shared by usage + * history and usage limits in one server process. + * + * macOS shows the access prompt on the server's own screen, which a remote + * client cannot answer, so a read gives up after a timeout. The Keychain read + * stays in flight: the next read joins it instead of stacking a second prompt, + * and picks up the token once someone allows access. + * + * @module provider-cursor/server/CursorKeychain + */ +import * as NodeModule from "node:module"; + +import * as Clock from "effect/Clock"; +import * as Context from "effect/Context"; +import * as Deferred from "effect/Deferred"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; + +const CACHE_MS = 5 * 60_000; +const DEFAULT_TIMEOUT = Duration.seconds(30); + +const requireForKeyring = NodeModule.createRequire(import.meta.url); + +/** Nobody answered the macOS Keychain prompt in time. */ +export class CursorKeychainTimeoutError extends Schema.TaggedError()( + "CursorKeychainTimeoutError", + {}, +) { + override get message(): string { + return "Timed out waiting for Keychain access."; + } +} + +export const isCursorKeychainTimeoutError = Schema.is(CursorKeychainTimeoutError); + +/** The Keychain could not be read. */ +export class CursorKeychainReadError extends Schema.TaggedError()( + "CursorKeychainReadError", + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Cursor Keychain credentials could not be read."; + } +} + +export class CursorKeychain extends Context.Service< + CursorKeychain, + { + /** The access token, or `null` when the Keychain holds no Cursor login. */ + readonly accessToken: Effect.Effect< + string | null, + CursorKeychainTimeoutError | CursorKeychainReadError + >; + } +>()("@t3tools/provider-cursor/server/CursorKeychain") {} + +/** Reads the Cursor CLI's default macOS credential without invoking the shared security binary. */ +const readKeychainEntry = Effect.tryPromise({ + try: async () => { + const { AsyncEntry } = requireForKeyring( + "@napi-rs/keyring", + ) as typeof import("@napi-rs/keyring"); + return (await new AsyncEntry("cursor-access-token", "cursor-user").getPassword()) ?? null; + }, + catch: (cause) => new CursorKeychainReadError({ cause }), +}); + +/** A Keychain backed by `read`, which tests replace. */ +export const make = ( + read: Effect.Effect = readKeychainEntry, + timeout: Duration.Input = DEFAULT_TIMEOUT, +): CursorKeychain["Service"] => { + let cached: { readonly token: string; readonly untilMs: number } | null = null; + let pending: Deferred.Deferred | null = null; + + /** Joins the read in flight, or starts one that outlives the caller. */ + const startRead = Effect.uninterruptible( + Effect.gen(function* () { + if (pending !== null) return pending; + const done = Deferred.makeUnsafe(); + pending = done; + yield* read.pipe( + Effect.tap((token) => + Clock.currentTimeMillis.pipe( + Effect.map((nowMs) => { + cached = token ? { token, untilMs: nowMs + CACHE_MS } : null; + }), + ), + ), + Effect.onExit((exit) => + Effect.suspend(() => { + pending = null; + return Deferred.done(done, exit); + }), + ), + Effect.forkDetach, + ); + return done; + }), + ); + + const accessToken = Effect.gen(function* () { + const nowMs = yield* Clock.currentTimeMillis; + if (cached !== null && cached.untilMs > nowMs) return cached.token; + const done = yield* startRead; + const token = yield* Deferred.await(done).pipe(Effect.timeoutOption(timeout)); + if (Option.isNone(token)) return yield* new CursorKeychainTimeoutError(); + return token.value; + }); + + return CursorKeychain.of({ accessToken }); +}; + +export const layer = Layer.sync(CursorKeychain, () => make()); diff --git a/packages/provider-cursor/src/server/CursorUsageAccounts.ts b/packages/provider-cursor/src/server/CursorUsageAccounts.ts new file mode 100644 index 000000000000..852d746aed00 --- /dev/null +++ b/packages/provider-cursor/src/server/CursorUsageAccounts.ts @@ -0,0 +1,342 @@ +/** + * CursorUsageAccounts - Cursor's account usage as a usage source, answered + * from a cache. + * + * Cursor's account API is slow, so a read answers from the cached account + * history and marks itself `refreshing` while a background refresh runs; + * `awaitRefresh` waits for that refresh instead. The caches persist in the + * server's state directory, so a restart only refetches the newest edge. See + * `accountCache`. + * + * @module provider-cursor/server/CursorUsageAccounts + */ +import * as NodeOS from "node:os"; + +import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; +import type { ProviderUsageScan } from "@t3tools/provider-core/server/usage"; +import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite"; +import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as Clock from "effect/Clock"; +import * as Context from "effect/Context"; +import * as Deferred from "effect/Deferred"; +import * as Effect from "effect/Effect"; +import * as Fiber from "effect/Fiber"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; +import * as Semaphore from "effect/Semaphore"; + +import { + CURSOR_ACCOUNT_CACHE_FILE_NAME, + CURSOR_ACCOUNT_TTL_MS, + cursorFetchRange, + decodeCursorAccountCaches, + encodeCursorAccountCaches, + isCursorCacheFresh, + mergeCursorFetch, + type CursorAccountCache, + type CursorCredentialSource, +} from "./accountCache.ts"; +import * as CursorAccountReader from "./CursorAccountReader.ts"; + +const CURSOR_ACCOUNT_READ_ERROR = "Cursor account usage could not be read."; + +/** The cache file is narrowed by hand in `accountCache`, so JSON is enough here. */ +const decodeCacheFile = Schema.decodeUnknownEffect( + Schema.fromJsonString(Schema.Unknown as unknown as Schema.Codec), +); + +export class CursorUsageAccounts extends Context.Service< + CursorUsageAccounts, + { + /** This environment's Cursor account usage source, if it has one to report. */ + readonly scan: (input: { + /** From the same settings snapshot as the rest of the usage read. */ + readonly keychainUsageEnabled: boolean; + readonly windowStartMs: number; + readonly retentionCutoffMs: number; + readonly awaitRefresh: boolean; + }) => Effect.Effect>; + /** Waits for every cache write scheduled so far, as a restart would need. */ + readonly awaitPersisted: Effect.Effect; + } +>()("@t3tools/provider-cursor/server/CursorUsageAccounts") {} + +const make = Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const host = yield* ProviderHost.ProviderHost; + const accountReader = yield* CursorAccountReader.CursorAccountReader; + const hostEnvironment = yield* HostProcessEnvironment; + const platform = yield* HostProcessPlatform; + + const cachePath = path.join(host.paths.stateDir, CURSOR_ACCOUNT_CACHE_FILE_NAME); + /** Account caches by credential source. */ + const caches = new Map(); + let cacheDirty = false; + /** + * The last failed refresh per credential source, standing for a TTL so a + * client refetching a broken login does not refetch Cursor each time. A + * `null` error means there is no login: no source to report. + */ + const failures = new Map(); + /** The refresh in flight per credential source, which every read joins. */ + const refreshes = new Map>(); + + /** Loads the persisted caches once; concurrent first readers await the same load. */ + const ensureLoaded = yield* Effect.cached( + fileSystem.readFileString(cachePath).pipe( + Effect.flatMap((raw) => decodeCacheFile(raw)), + Effect.catchCause(() => Effect.succeed(null)), + Effect.map((document) => { + for (const [key, cache] of decodeCursorAccountCaches(document)) caches.set(key, cache); + }), + ), + ); + + // Serialized so an older snapshot never lands after a newer one. The dirty + // flag is cleared before encoding, so a change while the write is in flight + // marks it dirty again; a failed write restores it so the next persist retries. + const persistLock = yield* Semaphore.make(1); + const persist = Effect.gen(function* () { + if (!cacheDirty) return; + cacheDirty = false; + yield* Effect.sync(() => encodeCursorAccountCaches(caches)).pipe( + Effect.flatMap((contents) => writeFileStringAtomically({ filePath: cachePath, contents })), + Effect.provideService(FileSystem.FileSystem, fileSystem), + Effect.provideService(Path.Path, path), + Effect.catchCause(() => + Effect.sync(() => { + cacheDirty = true; + }), + ), + ); + }).pipe(persistLock.withPermit, Effect.withSpan("CursorUsageAccounts.persist")); + + const pendingPersists = new Set>(); + /** Writes the caches in the background, after the read that dirtied them answers. */ + const schedulePersist = Effect.forkDetach(persist).pipe( + Effect.map((fiber) => { + pendingPersists.add(fiber); + fiber.addObserver(() => pendingPersists.delete(fiber)); + }), + ); + const awaitPersisted = Effect.suspend(() => Fiber.awaitAll([...pendingPersists])).pipe( + Effect.asVoid, + ); + // A write still running at shutdown finishes first, so the next start keeps it. + yield* Effect.addFinalizer(() => awaitPersisted); + + /** Fetches what one account cache is missing, then persists it if anything changed. */ + const refreshAccount = Effect.fn("CursorUsageAccounts.refreshAccount")(function* ( + credential: CursorCredentialSource, + credentialKey: string, + retentionStartMs: number, + ) { + const nowMs = yield* Clock.currentTimeMillis; + const fetchMissing = (cache: CursorAccountCache | undefined) => { + const range = cursorFetchRange(cache, retentionStartMs, nowMs); + return accountReader + .read(credential, range.sinceMs, range.untilMs) + .pipe(Effect.map((result) => ({ range, result }))); + }; + let base = caches.get(credentialKey); + let fetched = yield* fetchMissing(base); + // Another login's history replaces the cached account's, even if reading it fails. + if ( + base !== undefined && + fetched.result.accountKey !== null && + fetched.result.accountKey !== base.accountKey + ) { + caches.delete(credentialKey); + cacheDirty = true; + base = undefined; + fetched = yield* fetchMissing(base); + } + const { range, result } = fetched; + if (result.missing || result.error !== null || result.accountKey === null) { + failures.set(credentialKey, { + atMs: nowMs, + error: result.missing || result.error !== null ? result.error : CURSOR_ACCOUNT_READ_ERROR, + }); + yield* schedulePersist; + return; + } + const merged = mergeCursorFetch( + base, + result.accountKey, + range, + result.records, + nowMs, + retentionStartMs, + ); + caches.set(credentialKey, merged.cache); + failures.delete(credentialKey); + // An unchanged edge is not worth rewriting the file for: after a restart + // the cache just refetches a slightly wider edge. Persisting anyway retries + // an earlier failed write; it is a no-op when nothing is dirty. + if (merged.changed) cacheDirty = true; + yield* schedulePersist; + }); + + /** Joins the refresh in flight, or starts one. */ + const startRefresh = ( + credential: CursorCredentialSource, + credentialKey: string, + retentionStartMs: number, + ) => + // Enrollment and fork are atomic, so an interrupted caller cannot leave a + // registered refresh that nothing will finish. + Effect.uninterruptible( + Effect.gen(function* () { + const current = refreshes.get(credentialKey); + if (current !== undefined) return current; + const done = Deferred.makeUnsafe(); + refreshes.set(credentialKey, done); + // Detached: a departing client must not cancel a fetch later reads reuse. + yield* refreshAccount(credential, credentialKey, retentionStartMs).pipe( + Effect.catchCause(() => + Clock.currentTimeMillis.pipe( + Effect.map((atMs) => + failures.set(credentialKey, { atMs, error: CURSOR_ACCOUNT_READ_ERROR }), + ), + ), + ), + Effect.ensuring( + Effect.suspend(() => { + refreshes.delete(credentialKey); + return Deferred.succeed(done, undefined); + }), + ), + Effect.forkDetach, + ); + return done; + }), + ); + + /** + * The account source. A cache inside its TTL, or a refresh that failed + * inside it, answers directly. Otherwise a refresh starts: `awaitRefresh` + * waits for it, and anything else answers from the cache marked `refreshing`. + */ + const accountSource = Effect.fn("CursorUsageAccounts.accountSource")(function* ( + credential: CursorCredentialSource, + authPath: string, + windowStartMs: number, + retentionStartMs: number, + awaitRefresh: boolean, + ) { + // No saved login means there is no account source to report, not a setup error. + if ( + typeof credential === "string" && + !(yield* fileSystem.exists(credential).pipe(Effect.orElseSucceed(() => true))) + ) { + return null; + } + yield* ensureLoaded; + const credentialKey = typeof credential === "string" ? credential : "keychain"; + const nowMs = yield* Clock.currentTimeMillis; + const recentFailure = failures.get(credentialKey); + let refreshing = false; + if ( + (recentFailure === undefined || nowMs - recentFailure.atMs >= CURSOR_ACCOUNT_TTL_MS) && + !isCursorCacheFresh(caches.get(credentialKey), nowMs) + ) { + const refresh = yield* startRefresh(credential, credentialKey, retentionStartMs); + if (awaitRefresh) yield* Deferred.await(refresh); + else refreshing = true; + } + + const cache = caches.get(credentialKey); + const failure = refreshing ? undefined : failures.get(credentialKey); + const failureMessage = failure === undefined ? undefined : failure.error; + if (failureMessage === null) return null; + if (cache === undefined) { + return { + dir: authPath, + // Never combine a local fallback with another server's account-wide history. + ...(refreshing + ? { files: [], refreshing: true } + : { files: null, message: failureMessage ?? CURSOR_ACCOUNT_READ_ERROR }), + } satisfies ProviderUsageScan; + } + // The same account includes CLI and desktop history from every machine. + // A stable remote fingerprint prevents connected environments counting it twice. + const source = `cursor-account:${cache.accountKey}`; + return { + dir: source, + hostId: "cursor.com", + volumeId: cache.accountKey, + files: [ + { + path: source, + records: cache.records.filter((record) => record.timestampMs >= windowStartMs), + }, + ], + ...(failureMessage === undefined + ? { status: "ok" } + : { status: "partial", message: failureMessage }), + ...(refreshing ? { refreshing: true } : {}), + } satisfies ProviderUsageScan; + }); + + const scan: CursorUsageAccounts["Service"]["scan"] = Effect.fn("CursorUsageAccounts.scan")( + function* ({ keychainUsageEnabled, windowStartMs, retentionCutoffMs, awaitRefresh }) { + const home = NodeOS.homedir(); + const userHome = + (platform === "win32" ? hostEnvironment["USERPROFILE"] : hostEnvironment["HOME"]) || home; + const configHome = hostEnvironment["XDG_CONFIG_HOME"]?.trim(); + const cursorHome = + platform === "darwin" + ? path.join(userHome, "Library", "Application Support") + : platform === "win32" + ? hostEnvironment["APPDATA"] || path.join(userHome, "AppData", "Roaming") + : configHome && path.isAbsolute(configHome) + ? configHome + : path.join(userHome, ".config"); + const authPath = + platform === "darwin" + ? path.join(userHome, ".cursor", "auth.json") + : path.join(cursorHome, platform === "win32" ? "Cursor" : "cursor", "auth.json"); + const credentialStore = hostEnvironment["AGENT_CLI_CREDENTIAL_STORE"]; + const loginUnavailable = + Boolean(hostEnvironment["CURSOR_AUTH_TOKEN"]?.trim()) || + Boolean(hostEnvironment["CURSOR_API_KEY"]?.trim()) || + credentialStore === "memory"; + const useKeychain = platform === "darwin" && credentialStore !== "file"; + if (useKeychain && !loginUnavailable && !keychainUsageEnabled) { + return [ + { + dir: authPath, + volumeId: "", + files: null, + message: "Cursor account usage is off on this environment.", + action: "enableCursorKeychain", + }, + ]; + } + if (loginUnavailable) { + return [ + { + dir: authPath, + files: null, + message: "Cursor account history needs a Cursor CLI login on this server.", + }, + ]; + } + const source = yield* accountSource( + useKeychain ? { kind: "keychain" } : authPath, + authPath, + windowStartMs, + retentionCutoffMs, + awaitRefresh, + ); + return source === null ? [] : [source]; + }, + ); + + return CursorUsageAccounts.of({ scan, awaitPersisted }); +}); + +export const layer = Layer.effect(CursorUsageAccounts, make); diff --git a/apps/server/src/usage/cursorAccountCache.test.ts b/packages/provider-cursor/src/server/accountCache.test.ts similarity index 96% rename from apps/server/src/usage/cursorAccountCache.test.ts rename to packages/provider-cursor/src/server/accountCache.test.ts index da87e54c4157..56da1533ca0b 100644 --- a/apps/server/src/usage/cursorAccountCache.test.ts +++ b/packages/provider-cursor/src/server/accountCache.test.ts @@ -1,6 +1,6 @@ import { assert, describe, it } from "@effect/vitest"; -import { cursorFetchRange, mergeCursorFetch } from "./cursorAccountCache.ts"; +import { cursorFetchRange, mergeCursorFetch } from "./accountCache.ts"; import type { UsageRecord } from "@t3tools/provider-core/server/usage"; const DAY_MS = 24 * 60 * 60 * 1000; diff --git a/apps/server/src/usage/cursorAccountCache.ts b/packages/provider-cursor/src/server/accountCache.ts similarity index 98% rename from apps/server/src/usage/cursorAccountCache.ts rename to packages/provider-cursor/src/server/accountCache.ts index fd0a8ae6b08d..a7322328746b 100644 --- a/apps/server/src/usage/cursorAccountCache.ts +++ b/packages/provider-cursor/src/server/accountCache.ts @@ -10,9 +10,9 @@ * that range rather than adding to it, so each timestamp's records always come * from a single fetch and their occurrence indexes cannot collide. * - * @module cursorAccountCache + * @module provider-cursor/server/accountCache */ -import { cursorRateModel } from "./cursorUsageReader.ts"; +import { cursorRateModel } from "./accountUsage.ts"; import type { UsageRecord } from "@t3tools/provider-core/server/usage"; export type CursorCredentialSource = string | { readonly kind: "keychain" }; diff --git a/packages/provider-cursor/src/server/accountUsage.test.ts b/packages/provider-cursor/src/server/accountUsage.test.ts new file mode 100644 index 000000000000..32a0f41c0612 --- /dev/null +++ b/packages/provider-cursor/src/server/accountUsage.test.ts @@ -0,0 +1,340 @@ +// @effect-diagnostics nodeBuiltinImport:off - the reader under test reads a +// real CLI credential file from disk. +import * as NodeFSP from "node:fs/promises"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; + +import { afterEach, assert, beforeEach, describe, it } from "@effect/vitest"; + +import { CursorKeychainTimeoutError } from "./CursorKeychain.ts"; +import { readCursorAccountUsage } from "./accountUsage.ts"; + +let dir: string; + +beforeEach(async () => { + dir = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "usage-reader-test-")); +}); + +afterEach(async () => { + await NodeFSP.rm(dir, { recursive: true, force: true }); +}); + +describe("Cursor account history", () => { + it("reads Cursor account history with the default macOS Keychain login", async () => { + const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; + let keychainReads = 0; + const result = await readCursorAccountUsage( + { kind: "keychain" }, + 0, + 1781000000000, + async (_url, init) => { + assert.include(new Headers(init.headers).get("cookie") ?? "", "demo%3A%3A"); + return Response.json({ totalUsageEventsCount: 0, usageEventsDisplay: [] }); + }, + async () => { + keychainReads++; + return accessToken; + }, + ); + assert.strictEqual(keychainReads, 1); + assert.isNull(result.error); + assert.isFalse(result.missing); + assert.isNotNull(result.accountKey); + }); + + it("reads paginated Cursor account history including headless calls with separate cache tokens", async () => { + const authPath = NodePath.join(dir, "auth.json"); + const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo", exp: 4102444800 })).toString("base64url")}.signature`; + await NodeFSP.writeFile(authPath, JSON.stringify({ accessToken })); + const pages: number[] = []; + const signals: AbortSignal[] = []; + const request = async (url: string, init: RequestInit) => { + assert.strictEqual(String(url), "https://cursor.com/api/dashboard/get-filtered-usage-events"); + assert.strictEqual(init?.redirect, "error"); + const headers = new Headers(init?.headers); + assert.strictEqual(headers.get("origin"), "https://cursor.com"); + assert.include(headers.get("cookie") ?? "", "WorkosCursorSessionToken=demo%3A%3A"); + const body = JSON.parse(String(init?.body)); + pages.push(body.page); + if (init.signal) signals.push(init.signal); + return Response.json({ + totalUsageEventsCount: 1001, + usageEventsDisplay: Array.from({ length: body.page === 1 ? 1000 : 1 }, (_, index) => ({ + timestamp: String(1780000000000 + ((body.page - 1) * 1000 + index) * 1000), + model: "claude-sonnet-4-5", + conversationId: `conversation-${body.page}`, + isHeadless: body.page === 2, + chargedCents: 0, + tokenUsage: { + inputTokens: 10, + outputTokens: 5, + cacheReadTokens: 30, + cacheWriteTokens: 2, + totalCents: 25, + }, + })), + }); + }; + const result = await readCursorAccountUsage(authPath, 0, 1781000000000, request); + assert.isNull(result.error); + assert.deepStrictEqual(pages, [1, 2]); + assert.lengthOf(signals, 2); + assert.notStrictEqual(signals[0], signals[1]); + assert.strictEqual(result.records.length, 1001); + assert.strictEqual(result.records.at(-1)?.sessionId, "conversation-2"); + assert.deepStrictEqual(result.records[0]?.totals, { + uncachedInputTokens: 10, + cachedInputTokens: 30, + cacheCreationTokens: 2, + outputTokens: 5, + reasoningTokens: 0, + }); + assert.strictEqual(result.records[0]?.reportedCostUsd, 0.25); + assert.isFalse(result.accountKey?.includes("demo") ?? true); + }); + + it("reads Cursor account history beyond 100 pages", async () => { + const authPath = NodePath.join(dir, "auth.json"); + const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; + await NodeFSP.writeFile(authPath, JSON.stringify({ accessToken })); + const fullPage = Array.from({ length: 1000 }, () => ({ tokenUsage: null })); + let requests = 0; + const result = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => { + requests += 1; + return Response.json({ + totalUsageEventsCount: 100_001, + usageEventsDisplay: requests <= 100 ? fullPage : [{ tokenUsage: null }], + }); + }); + assert.isNull(result.error); + assert.strictEqual(requests, 101); + assert.deepStrictEqual(result.records, []); + }); + + it("accepts confirmed empty Cursor usage but rejects error envelopes", async () => { + const authPath = NodePath.join(dir, "auth.json"); + const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; + await NodeFSP.writeFile(authPath, JSON.stringify({ accessToken })); + for (const body of [ + {}, + { totalUsageEventsCount: 0 }, + { totalUsageEventsCount: 0, usageEventsDisplay: [] }, + ]) { + const result = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => + Response.json(body), + ); + assert.isNull(result.error); + assert.deepStrictEqual(result.records, []); + assert.isFalse(result.missing); + } + for (const body of [ + { error: "upstream error" }, + { detail: "unknown error envelope" }, + { totalUsageEventsCount: 0, error: "upstream error" }, + null, + [], + "invalid", + 0, + ]) { + const result = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => + Response.json(body), + ); + assert.isNotNull(result.error); + assert.deepStrictEqual(result.records, []); + } + }); + + it("requires a terminal Cursor page after a full page reaches the reported count", async () => { + const authPath = NodePath.join(dir, "auth.json"); + const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; + await NodeFSP.writeFile(authPath, JSON.stringify({ accessToken })); + let requests = 0; + const result = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => { + requests++; + return Response.json( + requests === 1 + ? { + totalUsageEventsCount: 1000, + usageEventsDisplay: Array.from({ length: 1000 }, (_, index) => ({ + timestamp: String(1780000000000 + index), + model: "gpt-5", + tokenUsage: { inputTokens: 10, outputTokens: 5 }, + })), + } + : { totalUsageEventsCount: 1000 }, + ); + }); + assert.isNull(result.error); + assert.strictEqual(result.records.length, 1000); + assert.strictEqual(requests, 2); + }); + + it("removes only count-proven Cursor boundary copies and preserves identical billed events", async () => { + const authPath = NodePath.join(dir, "auth.json"); + const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; + await NodeFSP.writeFile(authPath, JSON.stringify({ accessToken })); + const event = (index: number) => ({ + timestamp: String(1780000000000 + index), + model: "gpt-5", + tokenUsage: { inputTokens: 10, outputTokens: 5, totalCents: 1 }, + }); + for (const total of [2000, 2001]) { + let requests = 0; + const result = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => { + requests++; + return Response.json({ + totalUsageEventsCount: total, + usageEventsDisplay: + requests === 1 + ? Array.from({ length: 1000 }, (_, index) => event(index)) + : requests === 2 + ? Array.from({ length: 1000 }, (_, index) => event(999 + index)) + : [event(1999)], + }); + }); + assert.isNull(result.error); + assert.strictEqual(result.records.length, total); + assert.strictEqual(requests, 3); + assert.strictEqual(result.records.at(-1)?.timestampMs, 1780000001999); + assert.strictEqual( + result.records.filter((record) => record.timestampMs === 1780000000999).length, + total === 2000 ? 1 : 2, + ); + assert.strictEqual(new Set(result.records.map((record) => record.dedupeKey)).size, total); + } + let requests = 0; + const inconsistent = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => { + requests++; + return Response.json({ + totalUsageEventsCount: 1001, + usageEventsDisplay: + requests === 1 + ? Array.from({ length: 1000 }, (_, index) => event(index)) + : [event(500), event(1000)], + }); + }); + assert.isNotNull(inconsistent.error); + assert.deepStrictEqual(inconsistent.records, []); + }); + + it("does not present truncated Cursor account pages or authentication failures as complete history", async () => { + const authPath = NodePath.join(dir, "auth.json"); + const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo", exp: 4102444800 })).toString("base64url")}.signature`; + await NodeFSP.writeFile(authPath, JSON.stringify({ accessToken })); + const truncated = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => + Response.json({ totalUsageEventsCount: 101, usageEventsDisplay: [] }), + ); + assert.isNotNull(truncated.error); + assert.deepStrictEqual(truncated.records, []); + const denied = await readCursorAccountUsage( + authPath, + 0, + 1781000000000, + async () => new Response(accessToken, { status: 401 }), + ); + assert.isNotNull(denied.error); + assert.isFalse(denied.error?.includes(accessToken) ?? true); + assert.deepStrictEqual(denied.records, []); + let requested = false; + const missing = await readCursorAccountUsage( + NodePath.join(dir, "missing.json"), + 0, + 1781000000000, + async () => { + requested = true; + return Response.json({}); + }, + ); + assert.isTrue(missing.missing); + assert.isFalse(requested); + }); +}); + +describe("readCursorAccountUsage", () => { + it("asks for Keychain approval when the prompt goes unanswered", async () => { + const result = await readCursorAccountUsage( + { kind: "keychain" }, + 0, + 1, + () => Promise.reject(new Error("no network expected")), + () => Promise.reject(new CursorKeychainTimeoutError()), + ); + assert.deepStrictEqual(result, { + accountKey: null, + records: [], + missing: false, + error: "Allow Keychain access on the Mac running T3 Code, then refresh.", + }); + }); + + it("reads the pages behind the first together and keeps them in page order", async () => { + const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; + let inFlight = 0; + let mostInFlight = 0; + const result = await readCursorAccountUsage( + { kind: "keychain" }, + 0, + 1781000000000, + async (_url, init) => { + const page: number = JSON.parse(String(init.body)).page; + inFlight++; + mostInFlight = Math.max(mostInFlight, inFlight); + // Later pages answer first, so record order cannot come from arrival order. + for (let turn = page; turn < 9; turn++) await Promise.resolve(); + inFlight--; + return Response.json({ + totalUsageEventsCount: 8001, + usageEventsDisplay: Array.from({ length: page === 9 ? 1 : 1000 }, (_, index) => ({ + timestamp: String(1780000000000 + (page - 1) * 1000 + index), + model: "gpt-5", + tokenUsage: { inputTokens: 1 }, + })), + }); + }, + async () => accessToken, + ); + assert.isNull(result.error); + assert.strictEqual(mostInFlight, 6); + assert.deepStrictEqual( + result.records.map((record) => record.timestampMs), + Array.from({ length: 8001 }, (_, index) => 1780000000000 + index), + ); + }); + + it("ends like a page-by-page read when a page fails, leaving no request open", async () => { + const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; + const fullPage = { + totalUsageEventsCount: 8001, + usageEventsDisplay: Array.from({ length: 1000 }, () => ({ tokenUsage: null })), + }; + // Page 3 fails while the pages after it are still waiting on Cursor. + for (const [secondPage, error] of [ + [() => Response.json(fullPage), "Cursor account usage could not be read."], + [() => new Response(null, { status: 401 }), "Sign in to Cursor again to read account usage."], + ] as const) { + let open = 0; + const result = await readCursorAccountUsage( + { kind: "keychain" }, + 0, + 1781000000000, + (_url, init) => { + const page: number = JSON.parse(String(init.body)).page; + if (page === 1) return Promise.resolve(Response.json(fullPage)); + if (page === 2) return Promise.resolve(secondPage()); + if (page === 3) return Promise.reject(new Error("connection reset")); + open++; + return new Promise((_resolve, reject) => { + init.signal?.addEventListener("abort", () => { + open--; + reject(init.signal?.reason); + }); + }); + }, + async () => accessToken, + ); + assert.strictEqual(result.error, error); + assert.deepStrictEqual(result.records, []); + assert.strictEqual(open, 0); + } + }); +}); diff --git a/apps/server/src/usage/cursorUsageReader.ts b/packages/provider-cursor/src/server/accountUsage.ts similarity index 90% rename from apps/server/src/usage/cursorUsageReader.ts rename to packages/provider-cursor/src/server/accountUsage.ts index 90d2f6c60cc8..4ecab1ace876 100644 --- a/apps/server/src/usage/cursorUsageReader.ts +++ b/packages/provider-cursor/src/server/accountUsage.ts @@ -1,18 +1,19 @@ // Node fs reads CLI credentials, and crypto hashes account IDs for deduplication. // @effect-diagnostics nodeBuiltinImport:off +/** + * Cursor account usage from its dashboard API, read with the saved CLI or + * Keychain login. The dashboard covers CLI, desktop and headless usage from + * every machine on the account. + * + * @module provider-cursor/server/accountUsage + */ import * as NodeFSP from "node:fs/promises"; import * as NodeCrypto from "node:crypto"; import * as NodeTimersPromises from "node:timers/promises"; -import * as Context from "effect/Context"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; - import type { UsageRecord } from "@t3tools/provider-core/server/usage"; -import { - CursorKeychainTimeoutError, - readMacCursorAccessToken, -} from "@t3tools/provider-cursor/server"; + +import { isCursorKeychainTimeoutError } from "./CursorKeychain.ts"; function object(value: unknown): Record { return typeof value === "object" && value !== null && !Array.isArray(value) @@ -69,13 +70,17 @@ function boundaryOverlap(previous: readonly string[], current: readonly string[] return lengths.at(-1) ?? 0; } -/** Dashboard usage includes headless agents and reports fresh input separately from cache reads. */ +/** + * Dashboard usage includes headless agents and reports fresh input separately + * from cache reads. `keychainToken` is only called for a Keychain login. + */ export async function readCursorAccountUsage( credentialSource: string | { readonly kind: "keychain" }, sinceMs: number, endDate: number, request: (url: string, init: RequestInit) => Promise = globalThis.fetch, - keychainToken: () => Promise = readMacCursorAccessToken, + keychainToken: () => Promise = () => + Promise.reject(new Error("No Keychain reader")), ): Promise { let accessToken: unknown; try { @@ -93,7 +98,7 @@ export async function readCursorAccountUsage( ? null : typeof credentialSource === "string" ? "Cursor credentials could not be read." - : cause instanceof CursorKeychainTimeoutError + : isCursorKeychainTimeoutError(cause) ? "Allow Keychain access on the Mac running T3 Code, then refresh." : "Cursor Keychain credentials could not be read.", }; @@ -293,24 +298,3 @@ export async function readCursorAccountUsage( cancel.abort(); } } - -/** Reads one range of Cursor account usage. A service so tests can stand in for Cursor's API. */ -export class CursorAccountReader extends Context.Service< - CursorAccountReader, - { - readonly read: ( - credentialSource: string | { readonly kind: "keychain" }, - sinceMs: number, - untilMs: number, - ) => Effect.Effect; - } ->()("t3/usage/cursorUsageReader/CursorAccountReader") {} - -/** Reads Cursor's dashboard API with the saved CLI or Keychain login. */ -export const layer = Layer.succeed( - CursorAccountReader, - CursorAccountReader.of({ - read: (credentialSource, sinceMs, untilMs) => - Effect.promise(() => readCursorAccountUsage(credentialSource, sinceMs, untilMs)), - }), -); diff --git a/packages/provider-cursor/src/server/driver.test.ts b/packages/provider-cursor/src/server/driver.test.ts index ee0d2ba1ac85..8215f6563d48 100644 --- a/packages/provider-cursor/src/server/driver.test.ts +++ b/packages/provider-cursor/src/server/driver.test.ts @@ -15,6 +15,7 @@ import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost"; import { CursorDriver } from "./driver.ts"; import * as CursorAgentSdk from "./CursorAgentSdk.ts"; import * as CursorSdk from "./CursorSdk.ts"; +import * as CursorKeychain from "./CursorKeychain.ts"; import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter"; @@ -30,6 +31,11 @@ const layerTest = TestProviderHost.layer({ runBackgroundWork: false }).pipe( open: () => Effect.die("Maintenance resolution must not open a Cursor session"), }), ), + Layer.provideMerge( + Layer.succeed(CursorKeychain.CursorKeychain, { + accessToken: Effect.die("The driver test must not read the Keychain"), + }), + ), Layer.provideMerge( Layer.succeed( ProviderEventLoggers.ProviderEventLoggers, diff --git a/packages/provider-cursor/src/server/driver.ts b/packages/provider-cursor/src/server/driver.ts index 698ab5a88538..a9fa4b31acb4 100644 --- a/packages/provider-cursor/src/server/driver.ts +++ b/packages/provider-cursor/src/server/driver.ts @@ -41,6 +41,9 @@ import { makeCursorAuth } from "./auth.ts"; import * as CursorCredentialStore from "./credentialStore.ts"; import * as CursorAgentSdk from "./CursorAgentSdk.ts"; import * as CursorSdk from "./CursorSdk.ts"; +import * as CursorKeychain from "./CursorKeychain.ts"; +import * as CursorUsageAccounts from "./CursorUsageAccounts.ts"; + const decodeCursorSettings = Schema.decodeSync(CursorSettings); const isSdkRunnerError = Schema.is(CursorAgentSdk.CursorAgentSdkRunnerError); @@ -57,9 +60,14 @@ export type CursorDriverEnv = | Path.Path | HttpClient.HttpClient | CursorSdk.CursorSdk - | ProviderHost.ProviderHost; + | ProviderHost.ProviderHost + | CursorKeychain.CursorKeychain; -export const CursorDriver: ProviderDriver = { +export const CursorDriver: ProviderDriver< + CursorSettings, + CursorDriverEnv, + CursorUsageAccounts.CursorUsageAccounts +> = { driverKind: DRIVER_KIND, metadata: { displayName: "Cursor", @@ -67,12 +75,29 @@ export const CursorDriver: ProviderDriver = { }, configSchema: CursorSettings, defaultConfig: (): CursorSettings => decodeCursorSettings({}), + // One account source per environment: the host's Cursor CLI login. + usage: { + kind: "scan", + provider: "cursor", + scan: ({ settings, windowStartMs, retentionCutoffMs, awaitRefresh }) => + CursorUsageAccounts.CursorUsageAccounts.pipe( + Effect.flatMap((accounts) => + accounts.scan({ + keychainUsageEnabled: settings.cursorKeychainUsageEnabled, + windowStartMs, + retentionCutoffMs, + awaitRefresh, + }), + ), + ), + }, create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => Effect.gen(function* () { const host = yield* ProviderHost.ProviderHost; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const httpClient = yield* HttpClient.HttpClient; + const keychain = yield* CursorKeychain.CursorKeychain; const sdkRunner = yield* CursorAgentSdk.CursorAgentSdkRunner; const processEnv = mergeProviderInstanceEnvironment(environment); const continuationIdentity = defaultProviderContinuationIdentity({ @@ -221,6 +246,7 @@ export const CursorDriver: ProviderDriver = { Effect.provideService(HttpClient.HttpClient, httpClient), Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path), + Effect.provideService(CursorKeychain.CursorKeychain, keychain), Effect.map(stampSnapshot), Effect.provide(CursorSdkCatalog.layer), ); diff --git a/packages/provider-cursor/src/server/keychainToken.test.ts b/packages/provider-cursor/src/server/keychainToken.test.ts deleted file mode 100644 index 7f279bb60892..000000000000 --- a/packages/provider-cursor/src/server/keychainToken.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { assert, describe, it } from "@effect/vitest"; - -import { CursorKeychainTimeoutError, makeCachedCursorAccessTokenReader } from "./keychainToken.ts"; - -describe("Cursor Keychain reader", () => { - it("shares concurrent reads and rechecks after the cache expires", async () => { - let reads = 0; - let time = 0; - const read = makeCachedCursorAccessTokenReader( - async () => { - reads++; - return `token-${reads}`; - }, - () => time, - ); - assert.deepStrictEqual(await Promise.all([read(), read()]), ["token-1", "token-1"]); - assert.strictEqual(await read(), "token-1"); - assert.strictEqual(reads, 1); - time = 5 * 60_000; - assert.strictEqual(await read(), "token-2"); - }); - - it("gives up on an unanswered prompt and reuses it on the next read", async () => { - let reads = 0; - let allow: (token: string) => void = () => {}; - const read = makeCachedCursorAccessTokenReader( - () => { - reads++; - return new Promise((resolve) => { - allow = resolve; - }); - }, - () => 0, - 1, - ); - const error = await read().catch((cause: unknown) => cause); - assert.instanceOf(error, CursorKeychainTimeoutError); - const retry = read(); - allow("token"); - assert.strictEqual(await retry, "token"); - assert.strictEqual(reads, 1); - }); -}); diff --git a/packages/provider-cursor/src/server/keychainToken.ts b/packages/provider-cursor/src/server/keychainToken.ts deleted file mode 100644 index 2857683e4821..000000000000 --- a/packages/provider-cursor/src/server/keychainToken.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as NodeModule from "node:module"; - -const CACHE_MS = 5 * 60_000; - -const requireForKeyring = NodeModule.createRequire(import.meta.url); - -/** Rejected when nobody answers the macOS Keychain prompt in time. */ -export class CursorKeychainTimeoutError extends Error { - constructor() { - super("Timed out waiting for Keychain access."); - } -} - -/** - * Share one Keychain request across usage history and limits in this server process. - * - * macOS shows the access prompt on the server's own screen, which a remote - * client cannot answer, so callers give up after `timeoutMs`. The read stays in - * flight: the next call reuses it instead of stacking a second prompt, and picks - * up the token once someone allows access. - */ -export function makeCachedCursorAccessTokenReader( - read: () => Promise, - now: () => number = Date.now, - timeoutMs = 30_000, -): () => Promise { - let cached: { token: string; until: number } | null = null; - let pending: Promise | null = null; - return () => { - if (cached && cached.until > now()) return Promise.resolve(cached.token); - pending ??= read() - .then((token) => { - cached = token ? { token, until: now() + CACHE_MS } : null; - return token; - }) - .finally(() => { - pending = null; - }); - const deadline = AbortSignal.timeout(timeoutMs); - return Promise.race([ - pending, - new Promise((_, reject) => { - deadline.addEventListener("abort", () => reject(new CursorKeychainTimeoutError()), { - once: true, - }); - }), - ]); - }; -} - -/** Read the Cursor CLI's default macOS credential without invoking the shared security binary. */ -export const readMacCursorAccessToken = makeCachedCursorAccessTokenReader(async () => { - const { AsyncEntry } = requireForKeyring("@napi-rs/keyring") as typeof import("@napi-rs/keyring"); - return (await new AsyncEntry("cursor-access-token", "cursor-user").getPassword()) ?? null; -}); diff --git a/packages/provider-cursor/src/server/usageLimits.test.ts b/packages/provider-cursor/src/server/usageLimits.test.ts index ff765fcdec2b..bb3e38703037 100644 --- a/packages/provider-cursor/src/server/usageLimits.test.ts +++ b/packages/provider-cursor/src/server/usageLimits.test.ts @@ -6,10 +6,19 @@ import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as HttpClient from "effect/http/HttpClient"; import * as HttpClientResponse from "effect/http/HttpClientResponse"; +import * as CursorKeychain from "./CursorKeychain.ts"; import { cursorUsageResponseToLimits, readCursorUsageLimits } from "./usageLimits.ts"; -const withNodeServices = (effect: Effect.Effect) => - effect.pipe(Effect.provide(NodeServices.layer)); +const withNodeServices = ( + effect: Effect.Effect, + accessToken: CursorKeychain.CursorKeychain["Service"]["accessToken"] = Effect.die( + "must not read Keychain", + ), +) => + effect.pipe( + Effect.provideService(CursorKeychain.CursorKeychain, { accessToken }), + Effect.provide(NodeServices.layer), + ); describe("Cursor usage limits", () => { const checkedAt = "2026-09-16T00:00:00.000Z"; @@ -121,9 +130,6 @@ describe("Cursor usage limits", () => { ...(token ? { CURSOR_AUTH_TOKEN: token } : {}), }, false, - async () => { - throw new Error("must not read Keychain before opt-in"); - }, ).pipe( Effect.provideService(HostProcessPlatform, platform), Effect.provideService( @@ -195,7 +201,7 @@ describe("Cursor usage limits", () => { it.effect("reads the default macOS Cursor login from Keychain for limits", () => Effect.gen(function* () { const limits = yield* withNodeServices( - readCursorUsageLimits({ apiEndpoint: "" }, {}, true, async () => "keychain-token").pipe( + readCursorUsageLimits({ apiEndpoint: "" }, {}, true).pipe( Effect.provideService(HostProcessPlatform, "darwin"), Effect.provideService( FileSystem.FileSystem, @@ -216,6 +222,7 @@ describe("Cursor usage limits", () => { }), ), ), + Effect.succeed("keychain-token"), ); expect(limits.windows[0]?.usedPercent).toBe(42); }), @@ -224,15 +231,18 @@ describe("Cursor usage limits", () => { it.effect("reports a Keychain initialization failure without failing the provider refresh", () => Effect.gen(function* () { const limits = yield* withNodeServices( - readCursorUsageLimits({ apiEndpoint: "" }, {}, true, async () => { - throw new Error("Keychain initialization failed"); - }).pipe( + readCursorUsageLimits({ apiEndpoint: "" }, {}, true).pipe( Effect.provideService(HostProcessPlatform, "darwin"), Effect.provideService( HttpClient.HttpClient, HttpClient.make(() => Effect.die("must not request limits without a login")), ), ), + Effect.fail( + new CursorKeychain.CursorKeychainReadError({ + cause: new Error("Keychain initialization failed"), + }), + ), ); expect(limits.unavailable?.reason).toBe("probeFailed"); }), @@ -247,9 +257,7 @@ describe("Cursor usage limits", () => { ["", { CURSOR_API_ENDPOINT: "https://cursor-proxy.example" }], ] as const) { const limits = yield* withNodeServices( - readCursorUsageLimits({ apiEndpoint }, environment, true, async () => { - throw new Error("must not read Keychain for a custom endpoint"); - }).pipe( + readCursorUsageLimits({ apiEndpoint }, environment, true).pipe( Effect.provideService(HostProcessPlatform, "darwin"), Effect.provideService( HttpClient.HttpClient, diff --git a/packages/provider-cursor/src/server/usageLimits.ts b/packages/provider-cursor/src/server/usageLimits.ts index 78f096f8d335..57f74bc54488 100644 --- a/packages/provider-cursor/src/server/usageLimits.ts +++ b/packages/provider-cursor/src/server/usageLimits.ts @@ -17,7 +17,7 @@ import { makeUnavailableUsageLimits, makeUsageLimits, } from "@t3tools/provider-core/server/usageLimits"; -import { readMacCursorAccessToken } from "./keychainToken.ts"; +import * as CursorKeychain from "./CursorKeychain.ts"; const CursorCredentials = Schema.Struct({ accessToken: Schema.optional(Schema.String) }); const DEFAULT_CURSOR_API_ENDPOINT = "https://api2.cursor.sh"; @@ -66,7 +66,6 @@ export const readCursorUsageLimits = Effect.fn("readCursorUsageLimits")(function settings: Pick, environment: NodeJS.ProcessEnv = process.env, allowKeychain = false, - keychainToken: () => Promise = readMacCursorAccessToken, ) { const checkedAt = DateTime.formatIso(yield* DateTime.now); return yield* Effect.gen(function* () { @@ -106,7 +105,8 @@ export const readCursorUsageLimits = Effect.fn("readCursorUsageLimits")(function message: "Cursor account usage requires the default Cursor endpoint when using Keychain.", }); } - token = (yield* Effect.tryPromise(keychainToken))?.trim(); + const keychain = yield* CursorKeychain.CursorKeychain; + token = (yield* keychain.accessToken)?.trim(); } else if (!token) { const home = (platform === "win32" ? environment.USERPROFILE : environment.HOME) || NodeOS.homedir(); From 9a0766b0238f2c044a965dcba32c6cb7ea0b63cd Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 15:26:19 -0700 Subject: [PATCH 19/39] refactor(usage): Antigravity usage is a reader on its driver (#17579) Co-authored-by: Claude Opus 5.5 --- .../src/provider/Drivers/AntigravityDriver.ts | 8 +- .../src/provider/Drivers/AntigravityUsage.ts | 140 +++++++ .../Drivers/antigravityUsageReader.test.ts | 391 ++++++++++++++++++ .../Drivers}/antigravityUsageReader.ts | 0 apps/server/src/provider/builtInDrivers.ts | 3 +- apps/server/src/server.ts | 11 +- apps/server/src/usage/UsageService.test.ts | 2 + apps/server/src/usage/UsageService.ts | 117 +----- .../src/usage/usageTranscriptReader.test.ts | 373 ----------------- packages/provider-core/src/server/usage.ts | 4 +- 10 files changed, 564 insertions(+), 485 deletions(-) create mode 100644 apps/server/src/provider/Drivers/AntigravityUsage.ts create mode 100644 apps/server/src/provider/Drivers/antigravityUsageReader.test.ts rename apps/server/src/{usage => provider/Drivers}/antigravityUsageReader.ts (100%) diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.ts b/apps/server/src/provider/Drivers/AntigravityDriver.ts index f030341a83c1..c0812af2a6df 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.ts @@ -26,6 +26,7 @@ import { } from "../../textGeneration/AntigravityTextGeneration.ts"; import { makeAntigravityAuth, type AntigravityAuth } from "../AntigravityAuth.ts"; import * as AntigravityInstallation from "../AntigravityInstallation.ts"; +import * as AntigravityUsage from "./AntigravityUsage.ts"; import { antigravityAuthConfigIssue, antigravityAuthLabel, @@ -80,11 +81,16 @@ export type AntigravityDriverEnv = | ProviderEventLoggers.ProviderEventLoggers; /** Each instance owns its Google profile. Executable releases are shared by the environment. */ -export const AntigravityDriver: ProviderDriver = { +export const AntigravityDriver: ProviderDriver< + AntigravitySettings, + AntigravityDriverEnv, + AntigravityUsage.AntigravityUsage +> = { driverKind: DRIVER, metadata: { displayName: "Antigravity", supportsMultipleInstances: true }, configSchema: AntigravitySettings, defaultConfig: () => decodeSettings({}), + usage: AntigravityUsage.antigravityUsageReader, create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => Effect.gen(function* () { const crypto = yield* Crypto.Crypto; diff --git a/apps/server/src/provider/Drivers/AntigravityUsage.ts b/apps/server/src/provider/Drivers/AntigravityUsage.ts new file mode 100644 index 000000000000..d1958788ce2d --- /dev/null +++ b/apps/server/src/provider/Drivers/AntigravityUsage.ts @@ -0,0 +1,140 @@ +/** + * AntigravityUsage - Antigravity's usage history as a usage source. + * + * Antigravity keeps conversations in SQLite databases under its data + * directories and each instance's profile. Parsed databases stay in memory + * while a database and its WAL keep the same `(size, mtime, ctime)`, so a + * warm scan decodes only what changed. + * + * @module provider/Drivers/AntigravityUsage + */ +import * as NodeOS from "node:os"; + +import { type AntigravitySettings, UsageReadError } from "@t3tools/contracts"; +import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; +import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; +import type { + ProviderUsageInstance, + ProviderUsageReader, + ProviderUsageScan, +} from "@t3tools/provider-core/server/usage"; +import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as Context from "effect/Context"; +import * as Crypto from "effect/Crypto"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; + +import { resolveAntigravityInstanceDirectories } from "../antigravityAuthSupport.ts"; +import { makeAntigravityUsageCache, readAntigravityUsage } from "./antigravityUsageReader.ts"; + +export class AntigravityUsage extends Context.Service< + AntigravityUsage, + { + /** Every Antigravity store: the data directories and each configured instance's profile. */ + readonly scan: (input: { + readonly instances: ReadonlyArray>; + readonly windowStartMs: number; + }) => Effect.Effect, UsageReadError>; + } +>()("t3/provider/Drivers/AntigravityUsage") {} + +const make = Effect.gen(function* () { + const crypto = yield* Crypto.Crypto; + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const host = yield* ProviderHost.ProviderHost; + const hostEnvironment = yield* HostProcessEnvironment; + const cache = makeAntigravityUsageCache(); + + /** `ANTIGRAVITY_DATA_DIR` (comma-separated) or the defaults, canonicalized. */ + const dataRoots = Effect.gen(function* () { + const home = NodeOS.homedir(); + const configured = hostEnvironment["ANTIGRAVITY_DATA_DIR"] + ?.split(",") + .map((value) => value.trim()) + .filter(Boolean); + const defaults = [ + ...["antigravity", "antigravity-cli", "antigravity-ide", "antigravity-backup"].map((name) => + path.join(home, ".gemini", name), + ), + path.join(home, ".config", "antigravity"), + ]; + const canonical = new Set(); + for (const root of configured?.length ? configured : defaults) { + const resolved = path.resolve(expandHomePath(root)); + canonical.add( + yield* fileSystem.realPath(resolved).pipe(Effect.orElseSucceed(() => resolved)), + ); + } + return [...canonical]; + }); + + const scan: AntigravityUsage["Service"]["scan"] = Effect.fn("AntigravityUsage.scan")(function* ({ + instances, + windowStartMs, + }) { + const roots = yield* dataRoots; + // Only configured instances have a profile; the implicit default never ran. + for (const { instanceId } of instances.filter((instance) => instance.configured)) { + const directories = yield* resolveAntigravityInstanceDirectories( + host.paths.stateDir, + instanceId, + ).pipe( + Effect.provideService(Crypto.Crypto, crypto), + Effect.provideService(Path.Path, path), + Effect.mapError( + (cause) => + new UsageReadError({ + reason: "scanFailed", + detail: "Antigravity profile directory could not be resolved.", + cause, + }), + ), + ); + roots.push(path.join(directories.profile, "antigravity-acp")); + } + const conversationDirs = new Set(); + for (const root of roots) { + const resolvedRoot = yield* fileSystem.realPath(root).pipe(Effect.orElseSucceed(() => root)); + const nested = path.join(resolvedRoot, "conversations"); + const dir = (yield* fileSystem + .exists(nested) + .pipe(Effect.catchCause(() => Effect.succeed(false)))) + ? nested + : resolvedRoot; + conversationDirs.add(yield* fileSystem.realPath(dir).pipe(Effect.orElseSucceed(() => dir))); + } + const result = yield* Effect.promise(() => + readAntigravityUsage([...conversationDirs], windowStartMs, cache), + ); + const scanned: ProviderUsageScan[] = []; + for (const dir of conversationDirs) { + const exists = yield* fileSystem + .exists(dir) + .pipe(Effect.catchCause(() => Effect.succeed(false))); + const failed = result.errors.some( + (error) => error === dir || error.startsWith(`${dir}${path.sep}`), + ); + scanned.push({ + dir, + files: !exists && !failed ? null : result.files.filter((file) => file.root === dir), + status: failed ? "partial" : "ok", + ...(failed ? { message: "Some Antigravity history could not be read." } : {}), + }); + } + return scanned; + }); + + return AntigravityUsage.of({ scan }); +}); + +export const layer = Layer.effect(AntigravityUsage, make); + +export const antigravityUsageReader: ProviderUsageReader = { + kind: "scan", + provider: "antigravity", + scan: ({ instances, windowStartMs }) => + AntigravityUsage.pipe(Effect.flatMap((usage) => usage.scan({ instances, windowStartMs }))), +}; diff --git a/apps/server/src/provider/Drivers/antigravityUsageReader.test.ts b/apps/server/src/provider/Drivers/antigravityUsageReader.test.ts new file mode 100644 index 000000000000..edafd4ee89a6 --- /dev/null +++ b/apps/server/src/provider/Drivers/antigravityUsageReader.test.ts @@ -0,0 +1,391 @@ +// @effect-diagnostics nodeBuiltinImport:off - the reader under test opens real +// SQLite databases, mirroring the reader's own deliberate node:sqlite usage. +import * as NodeFSP from "node:fs/promises"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; +import * as NodeSqlite from "node:sqlite"; + +import { afterEach, assert, beforeEach, describe, it } from "@effect/vitest"; + +import { makeAntigravityUsageCache, readAntigravityUsage } from "./antigravityUsageReader.ts"; + +let dir: string; + +function protoNumber(field: number, value: number): number[] { + const varint = (number: number) => { + const bytes: number[] = []; + do { + const byte = number % 128; + number = Math.floor(number / 128); + bytes.push(byte + (number > 0 ? 128 : 0)); + } while (number > 0); + return bytes; + }; + return [...varint(field * 8), ...varint(value)]; +} + +function protoBytes(field: number, bytes: readonly number[]): number[] { + const encoded = protoNumber(field, bytes.length); + encoded[0] = encoded[0]! + 2; + return [...encoded, ...bytes]; +} + +function protoText(field: number, value: string): number[] { + return protoBytes(field, [...Buffer.from(value)]); +} + +beforeEach(async () => { + dir = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "usage-reader-test-")); +}); + +afterEach(async () => { + await NodeFSP.rm(dir, { recursive: true, force: true }); +}); + +describe("readAntigravityUsage", () => { + it("deduplicates Antigravity generation and step usage while preserving retry model and token buckets", async () => { + const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "session-1.db")); + const stamp = protoNumber(1, 1780000000); + const usage = [ + ...protoNumber(2, 100), + ...protoNumber(3, 40), + ...protoNumber(4, 5), + ...protoNumber(5, 20), + ...protoNumber(9, 10), + ...protoText(11, "response-1"), + ]; + const retry = [ + ...protoNumber(1, 1026), + ...protoNumber(2, 12), + ...protoNumber(3, 3), + ...protoText(11, "retry-1"), + ]; + const generation = protoBytes(1, [ + ...protoBytes(4, usage), + ...protoText(19, "Gemini 3 Pro"), + ...protoBytes(9, protoBytes(4, stamp)), + ]); + const step = [ + ...protoBytes(9, usage), + ...protoBytes(8, stamp), + ...protoBytes(28, protoBytes(2, retry)), + ]; + try { + db.exec( + "CREATE TABLE gen_metadata (idx INTEGER, data BLOB); CREATE TABLE steps (idx INTEGER, metadata BLOB)", + ); + db.prepare("INSERT INTO gen_metadata VALUES (?, ?)").run(0, new Uint8Array(generation)); + db.prepare("INSERT INTO steps VALUES (?, ?)").run(0, new Uint8Array(step)); + } finally { + db.close(); + } + const result = await readAntigravityUsage(dir, 0); + assert.deepStrictEqual(result.errors, []); + const records = result.files.flatMap((file) => file.records); + assert.strictEqual(records.length, 2); + const main = records.find((record) => record.model === "gemini-3-pro"); + assert.isDefined(main); + assert.strictEqual(main?.timestampMs, 1780000000000); + assert.strictEqual(main?.sessionId, "session-1"); + assert.deepStrictEqual(main?.totals, { + uncachedInputTokens: 100, + cachedInputTokens: 20, + cacheCreationTokens: 5, + outputTokens: 40, + reasoningTokens: 10, + }); + assert.strictEqual( + records.find((record) => record.model === "claude-opus-4-6")?.totals.uncachedInputTokens, + 12, + ); + assert.deepStrictEqual( + (await readAntigravityUsage(dir, 1780000000001)).files.flatMap((file) => file.records), + [], + ); + }); + + it("uses the matching Antigravity generation model for each model-less step", async () => { + const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "model-switch.db")); + try { + db.exec( + "CREATE TABLE gen_metadata (idx INTEGER, data BLOB); CREATE TABLE steps (idx INTEGER, metadata BLOB)", + ); + const generation = db.prepare("INSERT INTO gen_metadata VALUES (?, ?)"); + const step = db.prepare("INSERT INTO steps VALUES (?, ?)"); + for (const [idx, name] of ["Gemini 3 Pro", "Claude Opus 4.6"].entries()) { + generation.run(idx, new Uint8Array(protoBytes(1, protoText(19, name)))); + step.run(idx, new Uint8Array(protoBytes(9, protoNumber(2, 10 + idx)))); + } + } finally { + db.close(); + } + const result = await readAntigravityUsage(dir, 0); + assert.deepStrictEqual(result.errors, []); + assert.deepStrictEqual( + result.files.flatMap((file) => file.records).map((record) => record.model), + ["gemini-3-pro", "claude-opus-4-6"], + ); + }); + + it("merges Antigravity aliases that bridge previously separate step records", async () => { + const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "bridge.db")); + try { + db.exec( + "CREATE TABLE gen_metadata (idx INTEGER, data BLOB); CREATE TABLE steps (idx INTEGER, metadata BLOB)", + ); + const step = db.prepare("INSERT INTO steps VALUES (?, ?)"); + step.run( + 0, + new Uint8Array(protoBytes(9, [...protoNumber(2, 100), ...protoText(11, "response")])), + ); + step.run( + 1, + new Uint8Array(protoBytes(9, [...protoNumber(3, 40), ...protoText(12, "provider")])), + ); + db.prepare("INSERT INTO gen_metadata VALUES (?, ?)").run( + 0, + new Uint8Array( + protoBytes(1, [ + ...protoText(19, "Gemini 3 Pro"), + ...protoBytes(4, [ + ...protoNumber(2, 50), + ...protoNumber(5, 20), + ...protoText(11, "response"), + ...protoText(12, "provider"), + ]), + ]), + ), + ); + } finally { + db.close(); + } + const result = await readAntigravityUsage(dir, 0); + assert.deepStrictEqual(result.errors, []); + const records = result.files.flatMap((file) => file.records); + assert.strictEqual(records.length, 1); + assert.deepStrictEqual(records[0]?.totals, { + uncachedInputTokens: 100, + cachedInputTokens: 20, + cacheCreationTokens: 0, + outputTokens: 40, + reasoningTokens: 0, + }); + }); + + it("merges Antigravity provider and message aliases across configured roots while keeping original ownership", async () => { + const roots = [NodePath.join(dir, "first"), NodePath.join(dir, "second")]; + for (const [index, root] of roots.entries()) { + await NodeFSP.mkdir(root); + const db = new NodeSqlite.DatabaseSync(NodePath.join(root, `session-${index}.db`)); + try { + db.exec("CREATE TABLE steps (idx INTEGER, metadata BLOB)"); + for (const identity of [7, 12]) { + const usage = [ + ...protoNumber(1, 246), + ...protoNumber(2, index === 0 ? 100 : 150), + ...protoText(11, `response-${index}-${identity}`), + ...protoText(identity, `shared-${identity}`), + ]; + db.prepare("INSERT INTO steps VALUES (?, ?)").run( + identity, + new Uint8Array(protoBytes(9, usage)), + ); + } + } finally { + db.close(); + } + } + const result = await readAntigravityUsage(roots, 0); + assert.deepStrictEqual(result.errors, []); + assert.strictEqual(result.files.length, 2); + assert.strictEqual(result.files[0]?.root, roots[0]); + assert.strictEqual(result.files[0]?.records.length, 2); + assert.strictEqual(result.files[1]?.records.length, 0); + assert.deepStrictEqual( + result.files[0]?.records.map((record) => record.totals.uncachedInputTokens), + [150, 150], + ); + assert.isTrue(result.files[0]?.records.every((record) => record.sessionId === "session-0")); + }); + + it("upgrades Antigravity fallback timestamps before applying the date window", async () => { + for (const fallback of ["mtime", "trajectory"]) { + const path = NodePath.join(dir, `${fallback}.db`); + const db = new NodeSqlite.DatabaseSync(path); + try { + db.exec( + "CREATE TABLE gen_metadata (idx INTEGER, data BLOB); CREATE TABLE steps (idx INTEGER, metadata BLOB)", + ); + if (fallback === "trajectory") { + db.exec("CREATE TABLE trajectory_metadata_blob (data BLOB)"); + db.prepare("INSERT INTO trajectory_metadata_blob VALUES (?)").run( + new Uint8Array(protoBytes(2, protoNumber(1, 1780000200))), + ); + } + for (const [index, seconds] of [1780000000, 1780000200].entries()) { + const usage = [...protoNumber(2, 10), ...protoText(11, `${fallback}-${index}`)]; + db.prepare("INSERT INTO steps VALUES (?, ?)").run( + index, + new Uint8Array(protoBytes(9, usage)), + ); + db.prepare("INSERT INTO gen_metadata VALUES (?, ?)").run( + index, + new Uint8Array( + protoBytes(1, [ + ...protoBytes(4, usage), + ...protoBytes(9, protoBytes(4, protoNumber(1, seconds))), + ]), + ), + ); + } + } finally { + db.close(); + } + await NodeFSP.utimes(path, 1780000000, 1780000000); + } + const result = await readAntigravityUsage(dir, 1780000100000); + assert.deepStrictEqual(result.errors, []); + const records = result.files.flatMap((file) => file.records); + assert.strictEqual(records.length, 2); + assert.deepStrictEqual( + records.map((record) => record.timestampMs), + [1780000200000, 1780000200000], + ); + }); + + const antigravityGeneration = (responseId: string) => + new Uint8Array( + protoBytes(1, [ + ...protoBytes(4, [ + ...protoNumber(2, 100), + ...protoNumber(3, 40), + ...protoText(11, responseId), + ]), + ...protoText(19, "Gemini 3 Pro"), + ...protoBytes(9, protoBytes(4, protoNumber(1, 1780000000))), + ]), + ); + + it("reuses an unchanged Antigravity database instead of decoding it again", async () => { + const path = NodePath.join(dir, "session-1.db"); + const db = new NodeSqlite.DatabaseSync(path); + try { + db.exec("CREATE TABLE gen_metadata (idx INTEGER, data BLOB)"); + db.prepare("INSERT INTO gen_metadata VALUES (?, ?)").run(0, antigravityGeneration("r-1")); + const cache = makeAntigravityUsageCache(); + assert.deepStrictEqual((await readAntigravityUsage(dir, 0, cache)).errors, []); + + // An exclusive lock makes a fresh read fail without touching the file, so + // only a cache hit can still return the earlier records. + db.exec("BEGIN EXCLUSIVE"); + assert.strictEqual((await readAntigravityUsage(dir, 0)).errors.length, 1); + const cached = await readAntigravityUsage(dir, 0, cache); + assert.deepStrictEqual(cached.errors, []); + assert.deepStrictEqual( + cached.files.flatMap((file) => file.records).map((record) => record.dedupeKey), + ["antigravity:11:r-1"], + ); + db.exec("ROLLBACK"); + } finally { + db.close(); + } + }); + + it("rereads an Antigravity database rewritten with its size and mtime restored", async () => { + const path = NodePath.join(dir, "session-1.db"); + const db = new NodeSqlite.DatabaseSync(path); + try { + db.exec("CREATE TABLE gen_metadata (idx INTEGER, data BLOB)"); + db.prepare("INSERT INTO gen_metadata VALUES (?, ?)").run(0, antigravityGeneration("r-1")); + } finally { + db.close(); + } + await NodeFSP.utimes(path, 1780000000, 1780000000); + const cache = makeAntigravityUsageCache(); + assert.deepStrictEqual((await readAntigravityUsage(dir, 0, cache)).errors, []); + + // ctime has the kernel's timestamp granularity, which can be a few + // milliseconds, so repeat the forged rewrite until it lands on a later tick + // than the cached read, as any real rewrite does. + const cached = await NodeFSP.stat(path); + do { + await NodeFSP.writeFile(path, Buffer.alloc(cached.size)); + await NodeFSP.utimes(path, 1780000000, 1780000000); + } while ((await NodeFSP.stat(path)).ctimeMs === cached.ctimeMs); + const restored = await NodeFSP.stat(path); + assert.strictEqual(restored.size, cached.size); + assert.strictEqual(restored.mtimeMs, cached.mtimeMs); + const next = await readAntigravityUsage(dir, 0, cache); + assert.deepStrictEqual(next.errors, [path]); + assert.deepStrictEqual( + next.files.flatMap((file) => file.records), + [], + ); + }); + + it("rereads an Antigravity database when only its WAL changed", async () => { + const path = NodePath.join(dir, "session-1.db"); + const db = new NodeSqlite.DatabaseSync(path); + try { + db.exec( + "PRAGMA journal_mode = WAL; PRAGMA wal_autocheckpoint = 0; CREATE TABLE gen_metadata (idx INTEGER, data BLOB)", + ); + const insert = db.prepare("INSERT INTO gen_metadata VALUES (?, ?)"); + insert.run(0, antigravityGeneration("r-1")); + const cache = makeAntigravityUsageCache(); + const first = await readAntigravityUsage(dir, 0, cache); + assert.strictEqual(first.files.flatMap((file) => file.records).length, 1); + + const before = await NodeFSP.stat(path); + insert.run(1, antigravityGeneration("r-2")); + const after = await NodeFSP.stat(path); + assert.strictEqual(after.size, before.size); + assert.strictEqual(after.mtimeMs, before.mtimeMs); + + const next = await readAntigravityUsage(dir, 0, cache); + assert.deepStrictEqual( + next.files.flatMap((file) => file.records).map((record) => record.dedupeKey), + ["antigravity:11:r-1", "antigravity:11:r-2"], + ); + } finally { + db.close(); + } + }); + + it("reads Antigravity step-only stores and reports malformed databases", async () => { + const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "steps.db")); + try { + db.exec("CREATE TABLE steps (idx INTEGER, metadata BLOB)"); + const usage = [...protoNumber(1, 246), ...protoNumber(2, 10), ...protoNumber(3, 5)]; + db.prepare("INSERT INTO steps VALUES (?, ?)").run( + 0, + new Uint8Array([...protoBytes(9, usage), ...protoBytes(8, protoNumber(1, 1780000000))]), + ); + } finally { + db.close(); + } + await NodeFSP.writeFile(NodePath.join(dir, "broken.db"), "not a sqlite database"); + const result = await readAntigravityUsage(dir, 0); + assert.strictEqual(result.errors.length, 1); + assert.strictEqual(result.files.flatMap((file) => file.records)[0]?.model, "gemini-2.5-pro"); + assert.strictEqual(result.files.flatMap((file) => file.records)[0]?.totals.outputTokens, 5); + }); + + it("ignores large values in unused Antigravity protobuf fields", async () => { + const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "large-varint.db")); + try { + db.exec("CREATE TABLE steps (idx INTEGER, metadata BLOB)"); + const unusedField = [...protoNumber(99, 0).slice(0, -1), ...Array(9).fill(0xff), 0x01]; + const usage = [...protoNumber(1, 246), ...protoNumber(2, 10), ...unusedField]; + db.prepare("INSERT INTO steps VALUES (?, ?)").run(0, new Uint8Array(protoBytes(9, usage))); + } finally { + db.close(); + } + const result = await readAntigravityUsage(dir, 0); + assert.deepStrictEqual(result.errors, []); + assert.strictEqual( + result.files.flatMap((file) => file.records)[0]?.totals.uncachedInputTokens, + 10, + ); + }); +}); diff --git a/apps/server/src/usage/antigravityUsageReader.ts b/apps/server/src/provider/Drivers/antigravityUsageReader.ts similarity index 100% rename from apps/server/src/usage/antigravityUsageReader.ts rename to apps/server/src/provider/Drivers/antigravityUsageReader.ts diff --git a/apps/server/src/provider/builtInDrivers.ts b/apps/server/src/provider/builtInDrivers.ts index 9279ef326a25..532bd602d145 100644 --- a/apps/server/src/provider/builtInDrivers.ts +++ b/apps/server/src/provider/builtInDrivers.ts @@ -76,6 +76,7 @@ export type BuiltInUsageReadersEnv = | ProviderUsageReaderEnv | ProviderUsageReaderEnv | ProviderUsageReaderEnv + | ProviderUsageReaderEnv | ProviderUsageReaderEnv; /** @@ -85,4 +86,4 @@ export type BuiltInUsageReadersEnv = */ export const BUILT_IN_USAGE_DRIVERS: ReadonlyArray< AnyProviderDriver -> = [ClaudeDriver, CodexDriver, GrokDriver, OpenCodeDriver, CursorDriver]; +> = [ClaudeDriver, CodexDriver, GrokDriver, OpenCodeDriver, AntigravityDriver, CursorDriver]; diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 5659b130168d..80c5a73ac4c1 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -154,6 +154,7 @@ import * as NativeTelemetryClient from "./resourceTelemetry/NativeTelemetryClien import * as ResourceAttribution from "./resourceTelemetry/ResourceAttribution.ts"; import * as ResourceMonitorBinary from "./resourceTelemetry/ResourceMonitorBinary.ts"; import * as ResourceTelemetry from "./resourceTelemetry/ResourceTelemetry.ts"; +import * as AntigravityUsage from "./provider/Drivers/AntigravityUsage.ts"; import * as CursorAccountReader from "@t3tools/provider-cursor/server/CursorAccountReader"; import * as CursorKeychain from "@t3tools/provider-cursor/server/CursorKeychain"; import * as CursorUsageAccounts from "@t3tools/provider-cursor/server/CursorUsageAccounts"; @@ -255,10 +256,12 @@ const layerBackground = BackgroundPolicy.layer.pipe( const layerUsage = UsageService.layer.pipe( Layer.provide( - CursorUsageAccounts.layer.pipe( - Layer.provide(CursorAccountReader.layer.pipe(Layer.provide(CursorKeychain.layer))), - Layer.provide(ProviderHostLive.layer.pipe(Layer.provide(ServerSecretStore.layer))), - ), + Layer.mergeAll( + AntigravityUsage.layer, + CursorUsageAccounts.layer.pipe( + Layer.provide(CursorAccountReader.layer.pipe(Layer.provide(CursorKeychain.layer))), + ), + ).pipe(Layer.provide(ProviderHostLive.layer.pipe(Layer.provide(ServerSecretStore.layer)))), ), Layer.provide(layerServerSettings), ); diff --git a/apps/server/src/usage/UsageService.test.ts b/apps/server/src/usage/UsageService.test.ts index 83bace94138a..68d2e7c2bbf6 100644 --- a/apps/server/src/usage/UsageService.test.ts +++ b/apps/server/src/usage/UsageService.test.ts @@ -38,6 +38,7 @@ import * as CursorKeychain from "@t3tools/provider-cursor/server/CursorKeychain" import * as CursorUsageAccounts from "@t3tools/provider-cursor/server/CursorUsageAccounts"; import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; import * as BackgroundPolicy from "../background/BackgroundPolicy.ts"; +import * as AntigravityUsage from "../provider/Drivers/AntigravityUsage.ts"; import * as ProviderHostLive from "../provider/ProviderHostLive.ts"; import type { UsageRecord } from "@t3tools/provider-core/server/usage"; import * as UsageService from "./UsageService.ts"; @@ -127,6 +128,7 @@ const layerService = (input: { layerCursorUsageAccounts( CursorAccountReader.layer.pipe(Layer.provide(CursorKeychain.layer)), ).pipe( + Layer.provideMerge(AntigravityUsage.layer), Layer.provideMerge(ProviderHostLive.layer), Layer.provideMerge(Layer.mock(BackgroundPolicy.BackgroundPolicy)({})), Layer.provideMerge(Layer.mock(ServerSecretStore.ServerSecretStore)({})), diff --git a/apps/server/src/usage/UsageService.ts b/apps/server/src/usage/UsageService.ts index 2f48fc5abcab..ba65ba2887bb 100644 --- a/apps/server/src/usage/UsageService.ts +++ b/apps/server/src/usage/UsageService.ts @@ -1,16 +1,15 @@ /** - * UsageService - scans provider transcripts and returns priced usage buckets. + * UsageService - reads every driver's usage history and returns priced usage + * buckets. * - * The scan reads native session files and databases, including work driven - * outside T3 Code. Cursor's local records provide only partial coverage. + * Each built-in driver with a `usage` reader contributes its sources, including + * work driven outside T3 Code. A `transcripts` reader names JSONL directories + * that this service streams itself; a `scan` reader reads its own sources. * * JSONL transcripts are append-only, so parsed records are memoised per file by * `(size, mtime)`. A cold 30-day scan of ~1.4 GB lands around 2-3 seconds; warm * scans only reparse files that changed, and a file that merely grew resumes * from its cached parse position so only the appended bytes are read. - * OpenCode's SQLite reader queries the live database each scan so WAL writes - * remain visible. Antigravity databases are memoised in memory while the - * database and its WAL keep the same `(size, mtime, ctime)`. * * A scan reader with a slow source (an account API) may answer from its own * cache and mark itself `refreshing` while a background refresh runs; @@ -36,7 +35,6 @@ import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; import * as Cause from "effect/Cause"; import * as Clock from "effect/Clock"; 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"; @@ -52,9 +50,7 @@ import { HttpClient, HttpClientResponse } from "effect/http"; import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite"; import * as ServerConfig from "../config.ts"; -import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; import * as ServerSettings from "../serverSettings.ts"; -import { resolveAntigravityInstanceDirectories } from "../provider/antigravityAuthSupport.ts"; import { BUILT_IN_USAGE_DRIVERS, type BuiltInUsageReadersEnv } from "../provider/builtInDrivers.ts"; import type { ProviderDriver } from "@t3tools/provider-core/server/driver"; import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; @@ -63,7 +59,6 @@ import type { TranscriptUsageFormat, UsageRecord, } from "@t3tools/provider-core/server/usage"; -import { makeAntigravityUsageCache, readAntigravityUsage } from "./antigravityUsageReader.ts"; import { resolveModelAliases, UsageAggregator } from "./usageAggregation.ts"; import { createOverrideRateTable, parseRateTable, type RateTable } from "./usagePricing.ts"; import { @@ -227,7 +222,6 @@ const layerTest = Layer.succeed( ); export const make = Effect.gen(function* () { - const crypto = yield* Crypto.Crypto; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const config = yield* ServerConfig.ServerConfig; @@ -238,7 +232,6 @@ export const make = Effect.gen(function* () { const readerContext = yield* Effect.context(); const fileCache: ScanCache = new Map(); - const antigravityCache = makeAntigravityUsageCache(); const sourceCache = new Map(); let cacheDirty = false; const isWithinDirectory = (filePath: string, dir: string) => { @@ -639,22 +632,6 @@ export const make = Effect.gen(function* () { Effect.provideContext(readerContext), ); - const home = NodeOS.homedir(); - const envRoots = Effect.fnUntraced(function* (key: string, defaults: readonly string[]) { - const roots = hostEnvironment[key] - ?.split(",") - .map((value) => value.trim()) - .filter(Boolean); - const canonical = new Set(); - for (const root of roots?.length ? roots : defaults) { - const resolved = path.resolve(expandHomePath(root)); - canonical.add( - yield* fileSystem.realPath(resolved).pipe(Effect.orElseSucceed(() => resolved)), - ); - } - return [...canonical]; - }); - const scans = Effect.forEach( scanReaders, ({ driver, reader }) => @@ -686,83 +663,14 @@ export const make = Effect.gen(function* () { { concurrency: "unbounded" }, ); - const antigravity = Effect.gen(function* () { - const antigravityRoots = yield* envRoots("ANTIGRAVITY_DATA_DIR", [ - ...["antigravity", "antigravity-cli", "antigravity-ide", "antigravity-backup"].map((name) => - path.join(home, ".gemini", name), - ), - path.join(home, ".config", "antigravity"), - ]); - for (const [instanceId, instance] of Object.entries(settings.providerInstances)) { - if (instance.driver === "antigravity") { - const directories = yield* resolveAntigravityInstanceDirectories( - config.stateDir, - ProviderInstanceId.make(instanceId), - ).pipe( - Effect.provideService(Crypto.Crypto, crypto), - Effect.provideService(Path.Path, path), - Effect.mapError( - (cause) => - new UsageReadError({ - reason: "scanFailed", - detail: "Antigravity profile directory could not be resolved.", - cause, - }), - ), - ); - antigravityRoots.push(path.join(directories.profile, "antigravity-acp")); - } - } - const antigravityDirs = new Set(); - for (const root of antigravityRoots) { - const resolvedRoot = yield* fileSystem - .realPath(root) - .pipe(Effect.orElseSucceed(() => root)); - const nested = path.join(resolvedRoot, "conversations"); - const dir = (yield* fileSystem - .exists(nested) - .pipe(Effect.catchCause(() => Effect.succeed(false)))) - ? nested - : resolvedRoot; - antigravityDirs.add(yield* fileSystem.realPath(dir).pipe(Effect.orElseSucceed(() => dir))); - } - const result = yield* Effect.promise(() => - readAntigravityUsage([...antigravityDirs], windowStartMs, antigravityCache), - ); - const scanned: ScannedDir[] = []; - for (const dir of antigravityDirs) { - const exists = yield* fileSystem - .exists(dir) - .pipe(Effect.catchCause(() => Effect.succeed(false))); - const failed = result.errors.some( - (error) => error === dir || error.startsWith(`${dir}${path.sep}`), - ); - scanned.push({ - provider: "antigravity", - dir, - volumeId: yield* Effect.promise(() => readDirectoryVolumeId(dir)), - files: !exists && !failed ? null : result.files.filter((file) => file.root === dir), - status: failed ? "partial" : "ok", - ...(failed ? { message: "Some Antigravity history could not be read." } : {}), - }); - } - return scanned; - }); - // Independent sources scan together. Transcript directories go one at a // time, so open files stay at `TRANSCRIPT_READ_CONCURRENCY`. The result // keeps this order, since aggregation keeps the first copy of a duplicate. - const [transcripts, scanDirs, antigravityDirs] = yield* Effect.all( - [Effect.forEach(dirs, (dir) => scanTranscriptDir(dir, windowStartMs)), scans, antigravity], + const [transcripts, scanDirs] = yield* Effect.all( + [Effect.forEach(dirs, (dir) => scanTranscriptDir(dir, windowStartMs)), scans], { concurrency: "unbounded" }, ); - // Antigravity reads between the other scan readers until it has a reader of its own. - const scanned: readonly ScannedDir[] = [ - ...transcripts, - ...scanDirs.slice(0, -1).flat(), - ...antigravityDirs, - ...(scanDirs.at(-1) ?? []), - ]; + const scanned: readonly ScannedDir[] = [...transcripts, ...scanDirs.flat()]; return scanned; }); @@ -944,9 +852,10 @@ export const make = Effect.gen(function* () { }); /** - * In-flight scans by window and usage settings, so concurrent identical requests (the usage + * In-flight scans by window and settings, so concurrent identical requests (the usage * page open on two clients at once) share one scan instead of racing over - * the same corpus twice. + * the same corpus twice. Readers read settings of their own, so a scan is + * shared only under the same settings snapshot. */ const inflightScans = new Map>(); @@ -958,9 +867,7 @@ export const make = Effect.gen(function* () { input.resolution ?? "day", input.sinceTime ?? null, input.untilTime ?? null, - settings.usagePriceOverrides, - settings.usageModelAliases, - settings.cursorKeychainUsageEnabled, + settings, // A waiting read must never share a scan that answers with `refreshing`. input.awaitRefresh === true, ]); diff --git a/apps/server/src/usage/usageTranscriptReader.test.ts b/apps/server/src/usage/usageTranscriptReader.test.ts index 2cac91423697..e2c4ee1827fe 100644 --- a/apps/server/src/usage/usageTranscriptReader.test.ts +++ b/apps/server/src/usage/usageTranscriptReader.test.ts @@ -4,39 +4,14 @@ import * as NodeFSP from "node:fs/promises"; import * as NodeOS from "node:os"; import * as NodePath from "node:path"; -import * as NodeSqlite from "node:sqlite"; import { afterEach, assert, beforeEach, describe, it } from "@effect/vitest"; import { TEST_FORMATS } from "./usageTestFormats.ts"; import { readTranscriptRecords } from "./usageTranscriptReader.ts"; -import { makeAntigravityUsageCache, readAntigravityUsage } from "./antigravityUsageReader.ts"; let dir: string; -function protoNumber(field: number, value: number): number[] { - const varint = (number: number) => { - const bytes: number[] = []; - do { - const byte = number % 128; - number = Math.floor(number / 128); - bytes.push(byte + (number > 0 ? 128 : 0)); - } while (number > 0); - return bytes; - }; - return [...varint(field * 8), ...varint(value)]; -} - -function protoBytes(field: number, bytes: readonly number[]): number[] { - const encoded = protoNumber(field, bytes.length); - encoded[0] = encoded[0]! + 2; - return [...encoded, ...bytes]; -} - -function protoText(field: number, value: string): number[] { - return protoBytes(field, [...Buffer.from(value)]); -} - beforeEach(async () => { dir = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "usage-reader-test-")); }); @@ -249,351 +224,3 @@ describe("readTranscriptRecords resume", () => { ); }); }); - -describe("SQLite usage readers", () => { - it("deduplicates Antigravity generation and step usage while preserving retry model and token buckets", async () => { - const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "session-1.db")); - const stamp = protoNumber(1, 1780000000); - const usage = [ - ...protoNumber(2, 100), - ...protoNumber(3, 40), - ...protoNumber(4, 5), - ...protoNumber(5, 20), - ...protoNumber(9, 10), - ...protoText(11, "response-1"), - ]; - const retry = [ - ...protoNumber(1, 1026), - ...protoNumber(2, 12), - ...protoNumber(3, 3), - ...protoText(11, "retry-1"), - ]; - const generation = protoBytes(1, [ - ...protoBytes(4, usage), - ...protoText(19, "Gemini 3 Pro"), - ...protoBytes(9, protoBytes(4, stamp)), - ]); - const step = [ - ...protoBytes(9, usage), - ...protoBytes(8, stamp), - ...protoBytes(28, protoBytes(2, retry)), - ]; - try { - db.exec( - "CREATE TABLE gen_metadata (idx INTEGER, data BLOB); CREATE TABLE steps (idx INTEGER, metadata BLOB)", - ); - db.prepare("INSERT INTO gen_metadata VALUES (?, ?)").run(0, new Uint8Array(generation)); - db.prepare("INSERT INTO steps VALUES (?, ?)").run(0, new Uint8Array(step)); - } finally { - db.close(); - } - const result = await readAntigravityUsage(dir, 0); - assert.deepStrictEqual(result.errors, []); - const records = result.files.flatMap((file) => file.records); - assert.strictEqual(records.length, 2); - const main = records.find((record) => record.model === "gemini-3-pro"); - assert.isDefined(main); - assert.strictEqual(main?.timestampMs, 1780000000000); - assert.strictEqual(main?.sessionId, "session-1"); - assert.deepStrictEqual(main?.totals, { - uncachedInputTokens: 100, - cachedInputTokens: 20, - cacheCreationTokens: 5, - outputTokens: 40, - reasoningTokens: 10, - }); - assert.strictEqual( - records.find((record) => record.model === "claude-opus-4-6")?.totals.uncachedInputTokens, - 12, - ); - assert.deepStrictEqual( - (await readAntigravityUsage(dir, 1780000000001)).files.flatMap((file) => file.records), - [], - ); - }); - - it("uses the matching Antigravity generation model for each model-less step", async () => { - const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "model-switch.db")); - try { - db.exec( - "CREATE TABLE gen_metadata (idx INTEGER, data BLOB); CREATE TABLE steps (idx INTEGER, metadata BLOB)", - ); - const generation = db.prepare("INSERT INTO gen_metadata VALUES (?, ?)"); - const step = db.prepare("INSERT INTO steps VALUES (?, ?)"); - for (const [idx, name] of ["Gemini 3 Pro", "Claude Opus 4.6"].entries()) { - generation.run(idx, new Uint8Array(protoBytes(1, protoText(19, name)))); - step.run(idx, new Uint8Array(protoBytes(9, protoNumber(2, 10 + idx)))); - } - } finally { - db.close(); - } - const result = await readAntigravityUsage(dir, 0); - assert.deepStrictEqual(result.errors, []); - assert.deepStrictEqual( - result.files.flatMap((file) => file.records).map((record) => record.model), - ["gemini-3-pro", "claude-opus-4-6"], - ); - }); - - it("merges Antigravity aliases that bridge previously separate step records", async () => { - const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "bridge.db")); - try { - db.exec( - "CREATE TABLE gen_metadata (idx INTEGER, data BLOB); CREATE TABLE steps (idx INTEGER, metadata BLOB)", - ); - const step = db.prepare("INSERT INTO steps VALUES (?, ?)"); - step.run( - 0, - new Uint8Array(protoBytes(9, [...protoNumber(2, 100), ...protoText(11, "response")])), - ); - step.run( - 1, - new Uint8Array(protoBytes(9, [...protoNumber(3, 40), ...protoText(12, "provider")])), - ); - db.prepare("INSERT INTO gen_metadata VALUES (?, ?)").run( - 0, - new Uint8Array( - protoBytes(1, [ - ...protoText(19, "Gemini 3 Pro"), - ...protoBytes(4, [ - ...protoNumber(2, 50), - ...protoNumber(5, 20), - ...protoText(11, "response"), - ...protoText(12, "provider"), - ]), - ]), - ), - ); - } finally { - db.close(); - } - const result = await readAntigravityUsage(dir, 0); - assert.deepStrictEqual(result.errors, []); - const records = result.files.flatMap((file) => file.records); - assert.strictEqual(records.length, 1); - assert.deepStrictEqual(records[0]?.totals, { - uncachedInputTokens: 100, - cachedInputTokens: 20, - cacheCreationTokens: 0, - outputTokens: 40, - reasoningTokens: 0, - }); - }); - - it("merges Antigravity provider and message aliases across configured roots while keeping original ownership", async () => { - const roots = [NodePath.join(dir, "first"), NodePath.join(dir, "second")]; - for (const [index, root] of roots.entries()) { - await NodeFSP.mkdir(root); - const db = new NodeSqlite.DatabaseSync(NodePath.join(root, `session-${index}.db`)); - try { - db.exec("CREATE TABLE steps (idx INTEGER, metadata BLOB)"); - for (const identity of [7, 12]) { - const usage = [ - ...protoNumber(1, 246), - ...protoNumber(2, index === 0 ? 100 : 150), - ...protoText(11, `response-${index}-${identity}`), - ...protoText(identity, `shared-${identity}`), - ]; - db.prepare("INSERT INTO steps VALUES (?, ?)").run( - identity, - new Uint8Array(protoBytes(9, usage)), - ); - } - } finally { - db.close(); - } - } - const result = await readAntigravityUsage(roots, 0); - assert.deepStrictEqual(result.errors, []); - assert.strictEqual(result.files.length, 2); - assert.strictEqual(result.files[0]?.root, roots[0]); - assert.strictEqual(result.files[0]?.records.length, 2); - assert.strictEqual(result.files[1]?.records.length, 0); - assert.deepStrictEqual( - result.files[0]?.records.map((record) => record.totals.uncachedInputTokens), - [150, 150], - ); - assert.isTrue(result.files[0]?.records.every((record) => record.sessionId === "session-0")); - }); - - it("upgrades Antigravity fallback timestamps before applying the date window", async () => { - for (const fallback of ["mtime", "trajectory"]) { - const path = NodePath.join(dir, `${fallback}.db`); - const db = new NodeSqlite.DatabaseSync(path); - try { - db.exec( - "CREATE TABLE gen_metadata (idx INTEGER, data BLOB); CREATE TABLE steps (idx INTEGER, metadata BLOB)", - ); - if (fallback === "trajectory") { - db.exec("CREATE TABLE trajectory_metadata_blob (data BLOB)"); - db.prepare("INSERT INTO trajectory_metadata_blob VALUES (?)").run( - new Uint8Array(protoBytes(2, protoNumber(1, 1780000200))), - ); - } - for (const [index, seconds] of [1780000000, 1780000200].entries()) { - const usage = [...protoNumber(2, 10), ...protoText(11, `${fallback}-${index}`)]; - db.prepare("INSERT INTO steps VALUES (?, ?)").run( - index, - new Uint8Array(protoBytes(9, usage)), - ); - db.prepare("INSERT INTO gen_metadata VALUES (?, ?)").run( - index, - new Uint8Array( - protoBytes(1, [ - ...protoBytes(4, usage), - ...protoBytes(9, protoBytes(4, protoNumber(1, seconds))), - ]), - ), - ); - } - } finally { - db.close(); - } - await NodeFSP.utimes(path, 1780000000, 1780000000); - } - const result = await readAntigravityUsage(dir, 1780000100000); - assert.deepStrictEqual(result.errors, []); - const records = result.files.flatMap((file) => file.records); - assert.strictEqual(records.length, 2); - assert.deepStrictEqual( - records.map((record) => record.timestampMs), - [1780000200000, 1780000200000], - ); - }); - - const antigravityGeneration = (responseId: string) => - new Uint8Array( - protoBytes(1, [ - ...protoBytes(4, [ - ...protoNumber(2, 100), - ...protoNumber(3, 40), - ...protoText(11, responseId), - ]), - ...protoText(19, "Gemini 3 Pro"), - ...protoBytes(9, protoBytes(4, protoNumber(1, 1780000000))), - ]), - ); - - it("reuses an unchanged Antigravity database instead of decoding it again", async () => { - const path = NodePath.join(dir, "session-1.db"); - const db = new NodeSqlite.DatabaseSync(path); - try { - db.exec("CREATE TABLE gen_metadata (idx INTEGER, data BLOB)"); - db.prepare("INSERT INTO gen_metadata VALUES (?, ?)").run(0, antigravityGeneration("r-1")); - const cache = makeAntigravityUsageCache(); - assert.deepStrictEqual((await readAntigravityUsage(dir, 0, cache)).errors, []); - - // An exclusive lock makes a fresh read fail without touching the file, so - // only a cache hit can still return the earlier records. - db.exec("BEGIN EXCLUSIVE"); - assert.strictEqual((await readAntigravityUsage(dir, 0)).errors.length, 1); - const cached = await readAntigravityUsage(dir, 0, cache); - assert.deepStrictEqual(cached.errors, []); - assert.deepStrictEqual( - cached.files.flatMap((file) => file.records).map((record) => record.dedupeKey), - ["antigravity:11:r-1"], - ); - db.exec("ROLLBACK"); - } finally { - db.close(); - } - }); - - it("rereads an Antigravity database rewritten with its size and mtime restored", async () => { - const path = NodePath.join(dir, "session-1.db"); - const db = new NodeSqlite.DatabaseSync(path); - try { - db.exec("CREATE TABLE gen_metadata (idx INTEGER, data BLOB)"); - db.prepare("INSERT INTO gen_metadata VALUES (?, ?)").run(0, antigravityGeneration("r-1")); - } finally { - db.close(); - } - await NodeFSP.utimes(path, 1780000000, 1780000000); - const cache = makeAntigravityUsageCache(); - assert.deepStrictEqual((await readAntigravityUsage(dir, 0, cache)).errors, []); - - // ctime has the kernel's timestamp granularity, which can be a few - // milliseconds, so repeat the forged rewrite until it lands on a later tick - // than the cached read, as any real rewrite does. - const cached = await NodeFSP.stat(path); - do { - await NodeFSP.writeFile(path, Buffer.alloc(cached.size)); - await NodeFSP.utimes(path, 1780000000, 1780000000); - } while ((await NodeFSP.stat(path)).ctimeMs === cached.ctimeMs); - const restored = await NodeFSP.stat(path); - assert.strictEqual(restored.size, cached.size); - assert.strictEqual(restored.mtimeMs, cached.mtimeMs); - const next = await readAntigravityUsage(dir, 0, cache); - assert.deepStrictEqual(next.errors, [path]); - assert.deepStrictEqual( - next.files.flatMap((file) => file.records), - [], - ); - }); - - it("rereads an Antigravity database when only its WAL changed", async () => { - const path = NodePath.join(dir, "session-1.db"); - const db = new NodeSqlite.DatabaseSync(path); - try { - db.exec( - "PRAGMA journal_mode = WAL; PRAGMA wal_autocheckpoint = 0; CREATE TABLE gen_metadata (idx INTEGER, data BLOB)", - ); - const insert = db.prepare("INSERT INTO gen_metadata VALUES (?, ?)"); - insert.run(0, antigravityGeneration("r-1")); - const cache = makeAntigravityUsageCache(); - const first = await readAntigravityUsage(dir, 0, cache); - assert.strictEqual(first.files.flatMap((file) => file.records).length, 1); - - const before = await NodeFSP.stat(path); - insert.run(1, antigravityGeneration("r-2")); - const after = await NodeFSP.stat(path); - assert.strictEqual(after.size, before.size); - assert.strictEqual(after.mtimeMs, before.mtimeMs); - - const next = await readAntigravityUsage(dir, 0, cache); - assert.deepStrictEqual( - next.files.flatMap((file) => file.records).map((record) => record.dedupeKey), - ["antigravity:11:r-1", "antigravity:11:r-2"], - ); - } finally { - db.close(); - } - }); - - it("reads Antigravity step-only stores and reports malformed databases", async () => { - const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "steps.db")); - try { - db.exec("CREATE TABLE steps (idx INTEGER, metadata BLOB)"); - const usage = [...protoNumber(1, 246), ...protoNumber(2, 10), ...protoNumber(3, 5)]; - db.prepare("INSERT INTO steps VALUES (?, ?)").run( - 0, - new Uint8Array([...protoBytes(9, usage), ...protoBytes(8, protoNumber(1, 1780000000))]), - ); - } finally { - db.close(); - } - await NodeFSP.writeFile(NodePath.join(dir, "broken.db"), "not a sqlite database"); - const result = await readAntigravityUsage(dir, 0); - assert.strictEqual(result.errors.length, 1); - assert.strictEqual(result.files.flatMap((file) => file.records)[0]?.model, "gemini-2.5-pro"); - assert.strictEqual(result.files.flatMap((file) => file.records)[0]?.totals.outputTokens, 5); - }); - - it("ignores large values in unused Antigravity protobuf fields", async () => { - const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "large-varint.db")); - try { - db.exec("CREATE TABLE steps (idx INTEGER, metadata BLOB)"); - const unusedField = [...protoNumber(99, 0).slice(0, -1), ...Array(9).fill(0xff), 0x01]; - const usage = [...protoNumber(1, 246), ...protoNumber(2, 10), ...unusedField]; - db.prepare("INSERT INTO steps VALUES (?, ?)").run(0, new Uint8Array(protoBytes(9, usage))); - } finally { - db.close(); - } - const result = await readAntigravityUsage(dir, 0); - assert.deepStrictEqual(result.errors, []); - assert.strictEqual( - result.files.flatMap((file) => file.records)[0]?.totals.uncachedInputTokens, - 10, - ); - }); -}); diff --git a/packages/provider-core/src/server/usage.ts b/packages/provider-core/src/server/usage.ts index 62ecb5796b1b..0f1235d7037d 100644 --- a/packages/provider-core/src/server/usage.ts +++ b/packages/provider-core/src/server/usage.ts @@ -13,6 +13,7 @@ import type { ProviderInstanceId, ServerSettings, UsageProviderKind, + UsageReadError, UsageSource, UsageTokenTotals, } from "@t3tools/contracts"; @@ -177,7 +178,8 @@ export type ProviderUsageReader = | { readonly kind: "scan"; readonly provider: UsageProviderKind; + /** Fails only when the whole usage read cannot go on; an unreadable source is a status. */ readonly scan: ( input: ProviderUsageScanInput, - ) => Effect.Effect, never, R>; + ) => Effect.Effect, UsageReadError, R>; }; From b1ec4b36876e7f3ad03a1e9e921286ec7d58fe95 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 15:26:20 -0700 Subject: [PATCH 20/39] refactor(usage): usage readers use Effect FileSystem and SqlClient (#17615) Co-authored-by: Claude Opus 5.5 --- .../src/provider/Drivers/AntigravityUsage.ts | 5 +- .../Drivers/antigravityUsageReader.test.ts | 679 +++++++++-------- .../Drivers/antigravityUsageReader.ts | 295 +++++--- .../src/server/CursorAccountReader.ts | 27 +- .../src/server/accountUsage.test.ts | 687 ++++++++++-------- .../src/server/accountUsage.ts | 316 +++++--- .../src/server/usage.test.ts | 71 +- .../provider-opencode/src/server/usage.ts | 168 +++-- 8 files changed, 1280 insertions(+), 968 deletions(-) diff --git a/apps/server/src/provider/Drivers/AntigravityUsage.ts b/apps/server/src/provider/Drivers/AntigravityUsage.ts index d1958788ce2d..7b629c857c9a 100644 --- a/apps/server/src/provider/Drivers/AntigravityUsage.ts +++ b/apps/server/src/provider/Drivers/AntigravityUsage.ts @@ -106,8 +106,9 @@ const make = Effect.gen(function* () { : resolvedRoot; conversationDirs.add(yield* fileSystem.realPath(dir).pipe(Effect.orElseSucceed(() => dir))); } - const result = yield* Effect.promise(() => - readAntigravityUsage([...conversationDirs], windowStartMs, cache), + const result = yield* readAntigravityUsage([...conversationDirs], windowStartMs, cache).pipe( + Effect.provideService(FileSystem.FileSystem, fileSystem), + Effect.provideService(Path.Path, path), ); const scanned: ProviderUsageScan[] = []; for (const dir of conversationDirs) { diff --git a/apps/server/src/provider/Drivers/antigravityUsageReader.test.ts b/apps/server/src/provider/Drivers/antigravityUsageReader.test.ts index edafd4ee89a6..20c815fc9b32 100644 --- a/apps/server/src/provider/Drivers/antigravityUsageReader.test.ts +++ b/apps/server/src/provider/Drivers/antigravityUsageReader.test.ts @@ -1,15 +1,43 @@ -// @effect-diagnostics nodeBuiltinImport:off - the reader under test opens real -// SQLite databases, mirroring the reader's own deliberate node:sqlite usage. +// @effect-diagnostics-next-line nodeBuiltinImport:off - Effect's File.Info has no ctime or sub-millisecond mtime, which the cache tests compare. import * as NodeFSP from "node:fs/promises"; -import * as NodeOS from "node:os"; -import * as NodePath from "node:path"; -import * as NodeSqlite from "node:sqlite"; -import { afterEach, assert, beforeEach, describe, it } from "@effect/vitest"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, describe, it } from "@effect/vitest"; +import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; +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 Path from "effect/Path"; +import * as SqlClient from "effect/sql/SqlClient"; import { makeAntigravityUsageCache, readAntigravityUsage } from "./antigravityUsageReader.ts"; -let dir: string; +/** A writable connection that stays open until the enclosing scope closes. */ +const openDatabase = Effect.fn("openDatabase")(function* (filename: string) { + const context = yield* Layer.build(NodeSqliteClient.layer({ filename })); + const sql = Context.get(context, SqlClient.SqlClient); + return { + exec: (statement: string) => sql.unsafe(statement), + run: (statement: string, params: ReadonlyArray) => sql.unsafe(statement, params), + }; +}); + +/** Seeds `filename` and closes it, as a provider that has finished writing. */ +const seedDatabase = ( + filename: string, + seed: (db: Effect.Success>) => Effect.Effect, +) => + Effect.scoped( + Effect.gen(function* () { + yield* seed(yield* openDatabase(filename)); + }), + ); + +const tempDir = Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + return yield* fileSystem.makeTempDirectoryScoped({ prefix: "usage-reader-test-" }); +}); function protoNumber(field: number, value: number): number[] { const varint = (number: number) => { @@ -34,224 +62,241 @@ function protoText(field: number, value: string): number[] { return protoBytes(field, [...Buffer.from(value)]); } -beforeEach(async () => { - dir = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "usage-reader-test-")); -}); +const TABLES = + "CREATE TABLE gen_metadata (idx INTEGER, data BLOB); CREATE TABLE steps (idx INTEGER, metadata BLOB)"; -afterEach(async () => { - await NodeFSP.rm(dir, { recursive: true, force: true }); -}); +const createTables = (db: Effect.Success>) => + Effect.forEach(TABLES.split("; "), db.exec, { discard: true }); -describe("readAntigravityUsage", () => { - it("deduplicates Antigravity generation and step usage while preserving retry model and token buckets", async () => { - const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "session-1.db")); - const stamp = protoNumber(1, 1780000000); - const usage = [ - ...protoNumber(2, 100), - ...protoNumber(3, 40), - ...protoNumber(4, 5), - ...protoNumber(5, 20), - ...protoNumber(9, 10), - ...protoText(11, "response-1"), - ]; - const retry = [ - ...protoNumber(1, 1026), - ...protoNumber(2, 12), - ...protoNumber(3, 3), - ...protoText(11, "retry-1"), - ]; - const generation = protoBytes(1, [ - ...protoBytes(4, usage), - ...protoText(19, "Gemini 3 Pro"), - ...protoBytes(9, protoBytes(4, stamp)), - ]); - const step = [ - ...protoBytes(9, usage), - ...protoBytes(8, stamp), - ...protoBytes(28, protoBytes(2, retry)), - ]; - try { - db.exec( - "CREATE TABLE gen_metadata (idx INTEGER, data BLOB); CREATE TABLE steps (idx INTEGER, metadata BLOB)", - ); - db.prepare("INSERT INTO gen_metadata VALUES (?, ?)").run(0, new Uint8Array(generation)); - db.prepare("INSERT INTO steps VALUES (?, ?)").run(0, new Uint8Array(step)); - } finally { - db.close(); - } - const result = await readAntigravityUsage(dir, 0); - assert.deepStrictEqual(result.errors, []); - const records = result.files.flatMap((file) => file.records); - assert.strictEqual(records.length, 2); - const main = records.find((record) => record.model === "gemini-3-pro"); - assert.isDefined(main); - assert.strictEqual(main?.timestampMs, 1780000000000); - assert.strictEqual(main?.sessionId, "session-1"); - assert.deepStrictEqual(main?.totals, { - uncachedInputTokens: 100, - cachedInputTokens: 20, - cacheCreationTokens: 5, - outputTokens: 40, - reasoningTokens: 10, - }); - assert.strictEqual( - records.find((record) => record.model === "claude-opus-4-6")?.totals.uncachedInputTokens, - 12, - ); - assert.deepStrictEqual( - (await readAntigravityUsage(dir, 1780000000001)).files.flatMap((file) => file.records), - [], - ); - }); +const records = (result: Effect.Success>) => + result.files.flatMap((file) => file.records); - it("uses the matching Antigravity generation model for each model-less step", async () => { - const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "model-switch.db")); - try { - db.exec( - "CREATE TABLE gen_metadata (idx INTEGER, data BLOB); CREATE TABLE steps (idx INTEGER, metadata BLOB)", - ); - const generation = db.prepare("INSERT INTO gen_metadata VALUES (?, ?)"); - const step = db.prepare("INSERT INTO steps VALUES (?, ?)"); - for (const [idx, name] of ["Gemini 3 Pro", "Claude Opus 4.6"].entries()) { - generation.run(idx, new Uint8Array(protoBytes(1, protoText(19, name)))); - step.run(idx, new Uint8Array(protoBytes(9, protoNumber(2, 10 + idx)))); - } - } finally { - db.close(); - } - const result = await readAntigravityUsage(dir, 0); - assert.deepStrictEqual(result.errors, []); - assert.deepStrictEqual( - result.files.flatMap((file) => file.records).map((record) => record.model), - ["gemini-3-pro", "claude-opus-4-6"], - ); - }); +describe("readAntigravityUsage", () => { + it.effect( + "deduplicates Antigravity generation and step usage while preserving retry model and token buckets", + () => + Effect.gen(function* () { + const path = yield* Path.Path; + const dir = yield* tempDir; + const stamp = protoNumber(1, 1780000000); + const usage = [ + ...protoNumber(2, 100), + ...protoNumber(3, 40), + ...protoNumber(4, 5), + ...protoNumber(5, 20), + ...protoNumber(9, 10), + ...protoText(11, "response-1"), + ]; + const retry = [ + ...protoNumber(1, 1026), + ...protoNumber(2, 12), + ...protoNumber(3, 3), + ...protoText(11, "retry-1"), + ]; + const generation = protoBytes(1, [ + ...protoBytes(4, usage), + ...protoText(19, "Gemini 3 Pro"), + ...protoBytes(9, protoBytes(4, stamp)), + ]); + const step = [ + ...protoBytes(9, usage), + ...protoBytes(8, stamp), + ...protoBytes(28, protoBytes(2, retry)), + ]; + yield* seedDatabase(path.join(dir, "session-1.db"), (db) => + Effect.gen(function* () { + yield* createTables(db); + yield* db.run("INSERT INTO gen_metadata VALUES (?, ?)", [ + 0, + new Uint8Array(generation), + ]); + yield* db.run("INSERT INTO steps VALUES (?, ?)", [0, new Uint8Array(step)]); + }), + ); + const result = yield* readAntigravityUsage(dir, 0); + assert.deepStrictEqual(result.errors, []); + const read = records(result); + assert.strictEqual(read.length, 2); + const main = read.find((record) => record.model === "gemini-3-pro"); + assert.isDefined(main); + assert.strictEqual(main?.timestampMs, 1780000000000); + assert.strictEqual(main?.sessionId, "session-1"); + assert.deepStrictEqual(main?.totals, { + uncachedInputTokens: 100, + cachedInputTokens: 20, + cacheCreationTokens: 5, + outputTokens: 40, + reasoningTokens: 10, + }); + assert.strictEqual( + read.find((record) => record.model === "claude-opus-4-6")?.totals.uncachedInputTokens, + 12, + ); + assert.deepStrictEqual(records(yield* readAntigravityUsage(dir, 1780000000001)), []); + }).pipe(Effect.provide(NodeServices.layer)), + ); - it("merges Antigravity aliases that bridge previously separate step records", async () => { - const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "bridge.db")); - try { - db.exec( - "CREATE TABLE gen_metadata (idx INTEGER, data BLOB); CREATE TABLE steps (idx INTEGER, metadata BLOB)", + it.effect("uses the matching Antigravity generation model for each model-less step", () => + Effect.gen(function* () { + const path = yield* Path.Path; + const dir = yield* tempDir; + yield* seedDatabase(path.join(dir, "model-switch.db"), (db) => + Effect.gen(function* () { + yield* createTables(db); + for (const [idx, name] of ["Gemini 3 Pro", "Claude Opus 4.6"].entries()) { + yield* db.run("INSERT INTO gen_metadata VALUES (?, ?)", [ + idx, + new Uint8Array(protoBytes(1, protoText(19, name))), + ]); + yield* db.run("INSERT INTO steps VALUES (?, ?)", [ + idx, + new Uint8Array(protoBytes(9, protoNumber(2, 10 + idx))), + ]); + } + }), ); - const step = db.prepare("INSERT INTO steps VALUES (?, ?)"); - step.run( - 0, - new Uint8Array(protoBytes(9, [...protoNumber(2, 100), ...protoText(11, "response")])), - ); - step.run( - 1, - new Uint8Array(protoBytes(9, [...protoNumber(3, 40), ...protoText(12, "provider")])), - ); - db.prepare("INSERT INTO gen_metadata VALUES (?, ?)").run( - 0, - new Uint8Array( - protoBytes(1, [ - ...protoText(19, "Gemini 3 Pro"), - ...protoBytes(4, [ - ...protoNumber(2, 50), - ...protoNumber(5, 20), - ...protoText(11, "response"), - ...protoText(12, "provider"), - ]), - ]), - ), + const result = yield* readAntigravityUsage(dir, 0); + assert.deepStrictEqual(result.errors, []); + assert.deepStrictEqual( + records(result).map((record) => record.model), + ["gemini-3-pro", "claude-opus-4-6"], ); - } finally { - db.close(); - } - const result = await readAntigravityUsage(dir, 0); - assert.deepStrictEqual(result.errors, []); - const records = result.files.flatMap((file) => file.records); - assert.strictEqual(records.length, 1); - assert.deepStrictEqual(records[0]?.totals, { - uncachedInputTokens: 100, - cachedInputTokens: 20, - cacheCreationTokens: 0, - outputTokens: 40, - reasoningTokens: 0, - }); - }); + }).pipe(Effect.provide(NodeServices.layer)), + ); - it("merges Antigravity provider and message aliases across configured roots while keeping original ownership", async () => { - const roots = [NodePath.join(dir, "first"), NodePath.join(dir, "second")]; - for (const [index, root] of roots.entries()) { - await NodeFSP.mkdir(root); - const db = new NodeSqlite.DatabaseSync(NodePath.join(root, `session-${index}.db`)); - try { - db.exec("CREATE TABLE steps (idx INTEGER, metadata BLOB)"); - for (const identity of [7, 12]) { - const usage = [ - ...protoNumber(1, 246), - ...protoNumber(2, index === 0 ? 100 : 150), - ...protoText(11, `response-${index}-${identity}`), - ...protoText(identity, `shared-${identity}`), - ]; - db.prepare("INSERT INTO steps VALUES (?, ?)").run( - identity, - new Uint8Array(protoBytes(9, usage)), - ); - } - } finally { - db.close(); - } - } - const result = await readAntigravityUsage(roots, 0); - assert.deepStrictEqual(result.errors, []); - assert.strictEqual(result.files.length, 2); - assert.strictEqual(result.files[0]?.root, roots[0]); - assert.strictEqual(result.files[0]?.records.length, 2); - assert.strictEqual(result.files[1]?.records.length, 0); - assert.deepStrictEqual( - result.files[0]?.records.map((record) => record.totals.uncachedInputTokens), - [150, 150], - ); - assert.isTrue(result.files[0]?.records.every((record) => record.sessionId === "session-0")); - }); - - it("upgrades Antigravity fallback timestamps before applying the date window", async () => { - for (const fallback of ["mtime", "trajectory"]) { - const path = NodePath.join(dir, `${fallback}.db`); - const db = new NodeSqlite.DatabaseSync(path); - try { - db.exec( - "CREATE TABLE gen_metadata (idx INTEGER, data BLOB); CREATE TABLE steps (idx INTEGER, metadata BLOB)", - ); - if (fallback === "trajectory") { - db.exec("CREATE TABLE trajectory_metadata_blob (data BLOB)"); - db.prepare("INSERT INTO trajectory_metadata_blob VALUES (?)").run( - new Uint8Array(protoBytes(2, protoNumber(1, 1780000200))), - ); - } - for (const [index, seconds] of [1780000000, 1780000200].entries()) { - const usage = [...protoNumber(2, 10), ...protoText(11, `${fallback}-${index}`)]; - db.prepare("INSERT INTO steps VALUES (?, ?)").run( - index, - new Uint8Array(protoBytes(9, usage)), - ); - db.prepare("INSERT INTO gen_metadata VALUES (?, ?)").run( - index, + it.effect("merges Antigravity aliases that bridge previously separate step records", () => + Effect.gen(function* () { + const path = yield* Path.Path; + const dir = yield* tempDir; + yield* seedDatabase(path.join(dir, "bridge.db"), (db) => + Effect.gen(function* () { + yield* createTables(db); + yield* db.run("INSERT INTO steps VALUES (?, ?)", [ + 0, + new Uint8Array(protoBytes(9, [...protoNumber(2, 100), ...protoText(11, "response")])), + ]); + yield* db.run("INSERT INTO steps VALUES (?, ?)", [ + 1, + new Uint8Array(protoBytes(9, [...protoNumber(3, 40), ...protoText(12, "provider")])), + ]); + yield* db.run("INSERT INTO gen_metadata VALUES (?, ?)", [ + 0, new Uint8Array( protoBytes(1, [ - ...protoBytes(4, usage), - ...protoBytes(9, protoBytes(4, protoNumber(1, seconds))), + ...protoText(19, "Gemini 3 Pro"), + ...protoBytes(4, [ + ...protoNumber(2, 50), + ...protoNumber(5, 20), + ...protoText(11, "response"), + ...protoText(12, "provider"), + ]), ]), ), + ]); + }), + ); + const result = yield* readAntigravityUsage(dir, 0); + assert.deepStrictEqual(result.errors, []); + const read = records(result); + assert.strictEqual(read.length, 1); + assert.deepStrictEqual(read[0]?.totals, { + uncachedInputTokens: 100, + cachedInputTokens: 20, + cacheCreationTokens: 0, + outputTokens: 40, + reasoningTokens: 0, + }); + }).pipe(Effect.provide(NodeServices.layer)), + ); + + it.effect( + "merges Antigravity provider and message aliases across configured roots while keeping original ownership", + () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* tempDir; + const roots = [path.join(dir, "first"), path.join(dir, "second")]; + for (const [index, root] of roots.entries()) { + yield* fileSystem.makeDirectory(root); + yield* seedDatabase(path.join(root, `session-${index}.db`), (db) => + Effect.gen(function* () { + yield* db.exec("CREATE TABLE steps (idx INTEGER, metadata BLOB)"); + for (const identity of [7, 12]) { + const usage = [ + ...protoNumber(1, 246), + ...protoNumber(2, index === 0 ? 100 : 150), + ...protoText(11, `response-${index}-${identity}`), + ...protoText(identity, `shared-${identity}`), + ]; + yield* db.run("INSERT INTO steps VALUES (?, ?)", [ + identity, + new Uint8Array(protoBytes(9, usage)), + ]); + } + }), ); } - } finally { - db.close(); + const result = yield* readAntigravityUsage(roots, 0); + assert.deepStrictEqual(result.errors, []); + assert.strictEqual(result.files.length, 2); + assert.strictEqual(result.files[0]?.root, roots[0]); + assert.strictEqual(result.files[0]?.records.length, 2); + assert.strictEqual(result.files[1]?.records.length, 0); + assert.deepStrictEqual( + result.files[0]?.records.map((record) => record.totals.uncachedInputTokens), + [150, 150], + ); + assert.isTrue(result.files[0]?.records.every((record) => record.sessionId === "session-0")); + }).pipe(Effect.provide(NodeServices.layer)), + ); + + it.effect("upgrades Antigravity fallback timestamps before applying the date window", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* tempDir; + for (const fallback of ["mtime", "trajectory"]) { + const file = path.join(dir, `${fallback}.db`); + yield* seedDatabase(file, (db) => + Effect.gen(function* () { + yield* createTables(db); + if (fallback === "trajectory") { + yield* db.exec("CREATE TABLE trajectory_metadata_blob (data BLOB)"); + yield* db.run("INSERT INTO trajectory_metadata_blob VALUES (?)", [ + new Uint8Array(protoBytes(2, protoNumber(1, 1780000200))), + ]); + } + for (const [index, seconds] of [1780000000, 1780000200].entries()) { + const usage = [...protoNumber(2, 10), ...protoText(11, `${fallback}-${index}`)]; + yield* db.run("INSERT INTO steps VALUES (?, ?)", [ + index, + new Uint8Array(protoBytes(9, usage)), + ]); + yield* db.run("INSERT INTO gen_metadata VALUES (?, ?)", [ + index, + new Uint8Array( + protoBytes(1, [ + ...protoBytes(4, usage), + ...protoBytes(9, protoBytes(4, protoNumber(1, seconds))), + ]), + ), + ]); + } + }), + ); + yield* fileSystem.utimes(file, 1780000000, 1780000000); } - await NodeFSP.utimes(path, 1780000000, 1780000000); - } - const result = await readAntigravityUsage(dir, 1780000100000); - assert.deepStrictEqual(result.errors, []); - const records = result.files.flatMap((file) => file.records); - assert.strictEqual(records.length, 2); - assert.deepStrictEqual( - records.map((record) => record.timestampMs), - [1780000200000, 1780000200000], - ); - }); + const result = yield* readAntigravityUsage(dir, 1780000100000); + assert.deepStrictEqual(result.errors, []); + const read = records(result); + assert.strictEqual(read.length, 2); + assert.deepStrictEqual( + read.map((record) => record.timestampMs), + [1780000200000, 1780000200000], + ); + }).pipe(Effect.provide(NodeServices.layer)), + ); const antigravityGeneration = (responseId: string) => new Uint8Array( @@ -266,126 +311,138 @@ describe("readAntigravityUsage", () => { ]), ); - it("reuses an unchanged Antigravity database instead of decoding it again", async () => { - const path = NodePath.join(dir, "session-1.db"); - const db = new NodeSqlite.DatabaseSync(path); - try { - db.exec("CREATE TABLE gen_metadata (idx INTEGER, data BLOB)"); - db.prepare("INSERT INTO gen_metadata VALUES (?, ?)").run(0, antigravityGeneration("r-1")); + it.effect("reuses an unchanged Antigravity database instead of decoding it again", () => + Effect.gen(function* () { + const path = yield* Path.Path; + const dir = yield* tempDir; + const db = yield* openDatabase(path.join(dir, "session-1.db")); + yield* db.exec("CREATE TABLE gen_metadata (idx INTEGER, data BLOB)"); + yield* db.run("INSERT INTO gen_metadata VALUES (?, ?)", [0, antigravityGeneration("r-1")]); const cache = makeAntigravityUsageCache(); - assert.deepStrictEqual((await readAntigravityUsage(dir, 0, cache)).errors, []); + assert.deepStrictEqual((yield* readAntigravityUsage(dir, 0, cache)).errors, []); // An exclusive lock makes a fresh read fail without touching the file, so // only a cache hit can still return the earlier records. - db.exec("BEGIN EXCLUSIVE"); - assert.strictEqual((await readAntigravityUsage(dir, 0)).errors.length, 1); - const cached = await readAntigravityUsage(dir, 0, cache); + yield* db.exec("BEGIN EXCLUSIVE"); + assert.strictEqual((yield* readAntigravityUsage(dir, 0)).errors.length, 1); + const cached = yield* readAntigravityUsage(dir, 0, cache); assert.deepStrictEqual(cached.errors, []); assert.deepStrictEqual( - cached.files.flatMap((file) => file.records).map((record) => record.dedupeKey), + records(cached).map((record) => record.dedupeKey), ["antigravity:11:r-1"], ); - db.exec("ROLLBACK"); - } finally { - db.close(); - } - }); + yield* db.exec("ROLLBACK"); + }).pipe(Effect.provide(NodeServices.layer)), + ); - it("rereads an Antigravity database rewritten with its size and mtime restored", async () => { - const path = NodePath.join(dir, "session-1.db"); - const db = new NodeSqlite.DatabaseSync(path); - try { - db.exec("CREATE TABLE gen_metadata (idx INTEGER, data BLOB)"); - db.prepare("INSERT INTO gen_metadata VALUES (?, ?)").run(0, antigravityGeneration("r-1")); - } finally { - db.close(); - } - await NodeFSP.utimes(path, 1780000000, 1780000000); - const cache = makeAntigravityUsageCache(); - assert.deepStrictEqual((await readAntigravityUsage(dir, 0, cache)).errors, []); + it.effect("rereads an Antigravity database rewritten with its size and mtime restored", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const pathService = yield* Path.Path; + const dir = yield* tempDir; + const path = pathService.join(dir, "session-1.db"); + yield* seedDatabase(path, (db) => + Effect.gen(function* () { + yield* db.exec("CREATE TABLE gen_metadata (idx INTEGER, data BLOB)"); + yield* db.run("INSERT INTO gen_metadata VALUES (?, ?)", [ + 0, + antigravityGeneration("r-1"), + ]); + }), + ); + yield* fileSystem.utimes(path, 1780000000, 1780000000); + const cache = makeAntigravityUsageCache(); + assert.deepStrictEqual((yield* readAntigravityUsage(dir, 0, cache)).errors, []); - // ctime has the kernel's timestamp granularity, which can be a few - // milliseconds, so repeat the forged rewrite until it lands on a later tick - // than the cached read, as any real rewrite does. - const cached = await NodeFSP.stat(path); - do { - await NodeFSP.writeFile(path, Buffer.alloc(cached.size)); - await NodeFSP.utimes(path, 1780000000, 1780000000); - } while ((await NodeFSP.stat(path)).ctimeMs === cached.ctimeMs); - const restored = await NodeFSP.stat(path); - assert.strictEqual(restored.size, cached.size); - assert.strictEqual(restored.mtimeMs, cached.mtimeMs); - const next = await readAntigravityUsage(dir, 0, cache); - assert.deepStrictEqual(next.errors, [path]); - assert.deepStrictEqual( - next.files.flatMap((file) => file.records), - [], - ); - }); + // ctime has the kernel's timestamp granularity, which can be a few + // milliseconds, so repeat the forged rewrite until it lands on a later tick + // than the cached read, as any real rewrite does. + const stat = () => Effect.promise(() => NodeFSP.stat(path)); + const cached = yield* stat(); + do { + yield* fileSystem.writeFile(path, new Uint8Array(cached.size)); + yield* fileSystem.utimes(path, 1780000000, 1780000000); + } while ((yield* stat()).ctimeMs === cached.ctimeMs); + const restored = yield* stat(); + assert.strictEqual(restored.size, cached.size); + assert.strictEqual(restored.mtimeMs, cached.mtimeMs); + const next = yield* readAntigravityUsage(dir, 0, cache); + assert.deepStrictEqual(next.errors, [path]); + assert.deepStrictEqual(records(next), []); + }).pipe(Effect.provide(NodeServices.layer)), + ); - it("rereads an Antigravity database when only its WAL changed", async () => { - const path = NodePath.join(dir, "session-1.db"); - const db = new NodeSqlite.DatabaseSync(path); - try { - db.exec( - "PRAGMA journal_mode = WAL; PRAGMA wal_autocheckpoint = 0; CREATE TABLE gen_metadata (idx INTEGER, data BLOB)", - ); - const insert = db.prepare("INSERT INTO gen_metadata VALUES (?, ?)"); - insert.run(0, antigravityGeneration("r-1")); + it.effect("rereads an Antigravity database when only its WAL changed", () => + Effect.gen(function* () { + const pathService = yield* Path.Path; + const dir = yield* tempDir; + const path = pathService.join(dir, "session-1.db"); + const db = yield* openDatabase(path); + yield* db.exec("PRAGMA journal_mode = WAL"); + yield* db.exec("PRAGMA wal_autocheckpoint = 0"); + yield* db.exec("CREATE TABLE gen_metadata (idx INTEGER, data BLOB)"); + const insert = (idx: number, responseId: string) => + db.run("INSERT INTO gen_metadata VALUES (?, ?)", [idx, antigravityGeneration(responseId)]); + yield* insert(0, "r-1"); const cache = makeAntigravityUsageCache(); - const first = await readAntigravityUsage(dir, 0, cache); - assert.strictEqual(first.files.flatMap((file) => file.records).length, 1); + const first = yield* readAntigravityUsage(dir, 0, cache); + assert.strictEqual(records(first).length, 1); - const before = await NodeFSP.stat(path); - insert.run(1, antigravityGeneration("r-2")); - const after = await NodeFSP.stat(path); + const before = yield* Effect.promise(() => NodeFSP.stat(path)); + yield* insert(1, "r-2"); + const after = yield* Effect.promise(() => NodeFSP.stat(path)); assert.strictEqual(after.size, before.size); assert.strictEqual(after.mtimeMs, before.mtimeMs); - const next = await readAntigravityUsage(dir, 0, cache); + const next = yield* readAntigravityUsage(dir, 0, cache); assert.deepStrictEqual( - next.files.flatMap((file) => file.records).map((record) => record.dedupeKey), + records(next).map((record) => record.dedupeKey), ["antigravity:11:r-1", "antigravity:11:r-2"], ); - } finally { - db.close(); - } - }); + }).pipe(Effect.provide(NodeServices.layer)), + ); - it("reads Antigravity step-only stores and reports malformed databases", async () => { - const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "steps.db")); - try { - db.exec("CREATE TABLE steps (idx INTEGER, metadata BLOB)"); - const usage = [...protoNumber(1, 246), ...protoNumber(2, 10), ...protoNumber(3, 5)]; - db.prepare("INSERT INTO steps VALUES (?, ?)").run( - 0, - new Uint8Array([...protoBytes(9, usage), ...protoBytes(8, protoNumber(1, 1780000000))]), + it.effect("reads Antigravity step-only stores and reports malformed databases", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* tempDir; + yield* seedDatabase(path.join(dir, "steps.db"), (db) => + Effect.gen(function* () { + yield* db.exec("CREATE TABLE steps (idx INTEGER, metadata BLOB)"); + const usage = [...protoNumber(1, 246), ...protoNumber(2, 10), ...protoNumber(3, 5)]; + yield* db.run("INSERT INTO steps VALUES (?, ?)", [ + 0, + new Uint8Array([...protoBytes(9, usage), ...protoBytes(8, protoNumber(1, 1780000000))]), + ]); + }), ); - } finally { - db.close(); - } - await NodeFSP.writeFile(NodePath.join(dir, "broken.db"), "not a sqlite database"); - const result = await readAntigravityUsage(dir, 0); - assert.strictEqual(result.errors.length, 1); - assert.strictEqual(result.files.flatMap((file) => file.records)[0]?.model, "gemini-2.5-pro"); - assert.strictEqual(result.files.flatMap((file) => file.records)[0]?.totals.outputTokens, 5); - }); + yield* fileSystem.writeFileString(path.join(dir, "broken.db"), "not a sqlite database"); + const result = yield* readAntigravityUsage(dir, 0); + assert.strictEqual(result.errors.length, 1); + assert.strictEqual(records(result)[0]?.model, "gemini-2.5-pro"); + assert.strictEqual(records(result)[0]?.totals.outputTokens, 5); + }).pipe(Effect.provide(NodeServices.layer)), + ); - it("ignores large values in unused Antigravity protobuf fields", async () => { - const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "large-varint.db")); - try { - db.exec("CREATE TABLE steps (idx INTEGER, metadata BLOB)"); - const unusedField = [...protoNumber(99, 0).slice(0, -1), ...Array(9).fill(0xff), 0x01]; - const usage = [...protoNumber(1, 246), ...protoNumber(2, 10), ...unusedField]; - db.prepare("INSERT INTO steps VALUES (?, ?)").run(0, new Uint8Array(protoBytes(9, usage))); - } finally { - db.close(); - } - const result = await readAntigravityUsage(dir, 0); - assert.deepStrictEqual(result.errors, []); - assert.strictEqual( - result.files.flatMap((file) => file.records)[0]?.totals.uncachedInputTokens, - 10, - ); - }); + it.effect("ignores large values in unused Antigravity protobuf fields", () => + Effect.gen(function* () { + const path = yield* Path.Path; + const dir = yield* tempDir; + yield* seedDatabase(path.join(dir, "large-varint.db"), (db) => + Effect.gen(function* () { + yield* db.exec("CREATE TABLE steps (idx INTEGER, metadata BLOB)"); + const unusedField = [...protoNumber(99, 0).slice(0, -1), ...Array(9).fill(0xff), 0x01]; + const usage = [...protoNumber(1, 246), ...protoNumber(2, 10), ...unusedField]; + yield* db.run("INSERT INTO steps VALUES (?, ?)", [ + 0, + new Uint8Array(protoBytes(9, usage)), + ]); + }), + ); + const result = yield* readAntigravityUsage(dir, 0); + assert.deepStrictEqual(result.errors, []); + assert.strictEqual(records(result)[0]?.totals.uncachedInputTokens, 10); + }).pipe(Effect.provide(NodeServices.layer)), + ); }); diff --git a/apps/server/src/provider/Drivers/antigravityUsageReader.ts b/apps/server/src/provider/Drivers/antigravityUsageReader.ts index 9e91b6eacbb6..e48609cf874d 100644 --- a/apps/server/src/provider/Drivers/antigravityUsageReader.ts +++ b/apps/server/src/provider/Drivers/antigravityUsageReader.ts @@ -1,11 +1,14 @@ -// node:sqlite reads live conversation databases while Node fs discovers them. -// @effect-diagnostics nodeBuiltinImport:off +// @effect-diagnostics-next-line nodeBuiltinImport:off - Effect's File.Info has no ctime, which the cache fingerprint needs. import * as NodeFSP from "node:fs/promises"; -import * as NodePath from "node:path"; -import * as NodeSqlite from "node:sqlite"; -import * as NodeTimersPromises from "node:timers/promises"; import type { UsageRecord } from "@t3tools/provider-core/server/usage"; +import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; +import * as SqlClient from "effect/sql/SqlClient"; type FieldValue = number | bigint | Uint8Array; type Fields = Map; @@ -158,103 +161,136 @@ interface UsageCandidate { timestampQuality: number; } -async function readDatabase(path: string, fallbackTimestamp: number): Promise { - const db = new NodeSqlite.DatabaseSync(path, { readOnly: true }); - try { - db.exec("PRAGMA busy_timeout = 100; BEGIN"); +/** A conversation database whose usage tables are missing or undecodable. */ +class AntigravityDatabaseDecodeError extends Schema.TaggedError()( + "AntigravityDatabaseDecodeError", + { path: Schema.String, cause: Schema.optional(Schema.Defect()) }, +) { + override get message(): string { + return `Antigravity usage could not be decoded from ${this.path}.`; + } +} + +const readDatabase = Effect.fn("readDatabase")(function* ( + path: string, + sessionId: string, + fallbackTimestamp: number, +) { + const sql = yield* SqlClient.SqlClient; + const decode = (evaluate: () => A) => + Effect.try({ + try: evaluate, + catch: (cause) => new AntigravityDatabaseDecodeError({ path, cause }), + }); + yield* sql.unsafe("PRAGMA busy_timeout = 100"); + return yield* Effect.gen(function* () { const tables = new Set( - db - .prepare("SELECT name FROM sqlite_master WHERE type = 'table'") - .all() - .map((row) => row.name), + (yield* sql.unsafe<{ readonly name: unknown }>( + "SELECT name FROM sqlite_master WHERE type = 'table'", + )).map((row) => row.name), ); if (!tables.has("gen_metadata") && !tables.has("steps")) { - throw new Error("Missing Antigravity usage tables"); + return yield* new AntigravityDatabaseDecodeError({ path }); } - const readMetadata = async (query: string, column: string, step: boolean) => { + const readMetadata = Effect.fnUntraced(function* ( + query: string, + column: string, + step: boolean, + ) { const entries: Array<{ idx: number; entry: Metadata }> = []; - for (const row of db.prepare(query).iterate()) { - if (typeof row.idx !== "number") throw new Error("Invalid Antigravity metadata index"); - entries.push({ idx: row.idx, entry: metadata(blob(row[column]), step) }); - if (entries.length % 256 === 0) await NodeTimersPromises.setImmediate(); + for (const row of yield* sql.unsafe>(query)) { + const idx = row.idx; + if (typeof idx !== "number") { + return yield* new AntigravityDatabaseDecodeError({ path }); + } + entries.push({ idx, entry: yield* decode(() => metadata(blob(row[column]), step)) }); + if (entries.length % 256 === 0) yield* Effect.yieldNow; } return entries; - }; + }); const generations = tables.has("gen_metadata") - ? await readMetadata("SELECT idx, data FROM gen_metadata ORDER BY idx", "data", false) + ? yield* readMetadata("SELECT idx, data FROM gen_metadata ORDER BY idx", "data", false) : []; let trajectoryTimestamp: number | null = null; if (tables.has("trajectory_metadata_blob")) { - for (const row of db.prepare("SELECT data FROM trajectory_metadata_blob").iterate()) { - trajectoryTimestamp ??= timestamp(nested(fields(blob(row.data)), 2)); + for (const row of yield* sql.unsafe<{ readonly data: unknown }>( + "SELECT data FROM trajectory_metadata_blob", + )) { + trajectoryTimestamp ??= yield* decode(() => timestamp(nested(fields(blob(row.data)), 2))); } } const steps = tables.has("steps") - ? await readMetadata( + ? yield* readMetadata( "SELECT idx, metadata FROM steps WHERE metadata IS NOT NULL ORDER BY idx", "metadata", true, ) : []; - const sessionId = NodePath.basename(path, ".db"); - const records: UsageCandidate[] = []; - const generationModels = new Map(generations.map(({ idx, entry }) => [idx, entry.model])); - for (const [source, entries] of [ - ["step", steps], - ["generation", generations], - ] as const) { - for (const [index, { idx, entry }] of entries.entries()) { - for (const [usageIndex, usage] of entry.usages.entries()) { - const outputTokens = Math.max( - numberAt(usage, 3), - numberAt(usage, 9) + numberAt(usage, 10), - ); - const totals = { - uncachedInputTokens: numberAt(usage, 2), - cachedInputTokens: numberAt(usage, 5), - cacheCreationTokens: numberAt(usage, 4), - outputTokens, - reasoningTokens: Math.min(outputTokens, numberAt(usage, 9)), - }; - if ( - totals.uncachedInputTokens + - totals.cachedInputTokens + - totals.cacheCreationTokens + - outputTokens === - 0 - ) - continue; - const keys = ([11, 12, 7] as const).flatMap((key) => { - const id = textAt(usage, key); - return id ? [`antigravity:${key}:${id}`] : []; - }); - const record: UsageRecord = { - provider: "antigravity", - sessionId, - timestampMs: entry.timestampMs ?? trajectoryTimestamp ?? fallbackTimestamp, - model: - MODEL_IDS[numberAt(usage, 1)] || - entry.model || - (source === "step" ? generationModels.get(idx) : "") || - modelName("", numberAt(usage, 1)) || - "antigravity-unknown", - totals, - reportedCostUsd: null, - speed: "standard", - dedupeKey: keys[0] ?? `antigravity:${sessionId}:${source}:${index}:${usageIndex}`, - }; - records.push({ - record, - keys, - timestampQuality: entry.timestampMs !== null ? 2 : trajectoryTimestamp !== null ? 1 : 0, - }); - } + return yield* decode(() => + usageCandidates(sessionId, generations, steps, trajectoryTimestamp, fallbackTimestamp), + ); + }).pipe(sql.withTransaction); +}); + +function usageCandidates( + sessionId: string, + generations: ReadonlyArray<{ idx: number; entry: Metadata }>, + steps: ReadonlyArray<{ idx: number; entry: Metadata }>, + trajectoryTimestamp: number | null, + fallbackTimestamp: number, +): UsageCandidate[] { + const records: UsageCandidate[] = []; + const generationModels = new Map(generations.map(({ idx, entry }) => [idx, entry.model])); + for (const [source, entries] of [ + ["step", steps], + ["generation", generations], + ] as const) { + for (const [index, { idx, entry }] of entries.entries()) { + for (const [usageIndex, usage] of entry.usages.entries()) { + const outputTokens = Math.max(numberAt(usage, 3), numberAt(usage, 9) + numberAt(usage, 10)); + const totals = { + uncachedInputTokens: numberAt(usage, 2), + cachedInputTokens: numberAt(usage, 5), + cacheCreationTokens: numberAt(usage, 4), + outputTokens, + reasoningTokens: Math.min(outputTokens, numberAt(usage, 9)), + }; + if ( + totals.uncachedInputTokens + + totals.cachedInputTokens + + totals.cacheCreationTokens + + outputTokens === + 0 + ) + continue; + const keys = ([11, 12, 7] as const).flatMap((key) => { + const id = textAt(usage, key); + return id ? [`antigravity:${key}:${id}`] : []; + }); + const record: UsageRecord = { + provider: "antigravity", + sessionId, + timestampMs: entry.timestampMs ?? trajectoryTimestamp ?? fallbackTimestamp, + model: + MODEL_IDS[numberAt(usage, 1)] || + entry.model || + (source === "step" ? generationModels.get(idx) : "") || + modelName("", numberAt(usage, 1)) || + "antigravity-unknown", + totals, + reportedCostUsd: null, + speed: "standard", + dedupeKey: keys[0] ?? `antigravity:${sessionId}:${source}:${index}:${usageIndex}`, + }; + records.push({ + record, + keys, + timestampQuality: entry.timestampMs !== null ? 2 : trajectoryTimestamp !== null ? 1 : 0, + }); } } - return records; - } finally { - db.close(); } + return records; } interface CachedDatabase { @@ -275,11 +311,13 @@ export const makeAntigravityUsageCache = () => new Map() * filtering. With a cache, databases unchanged since the previous read are not * decoded again. */ -export async function readAntigravityUsage( +export const readAntigravityUsage = Effect.fn("readAntigravityUsage")(function* ( conversationsDirectories: string | readonly string[], sinceMs: number, cache?: Map, ) { + const fileSystem = yield* FileSystem.FileSystem; + const pathService = yield* Path.Path; const roots = typeof conversationsDirectories === "string" ? [conversationsDirectories] @@ -350,50 +388,69 @@ export async function readAntigravityUsage( } }; const visited = new Set(); - const walk = async (directory: string, root: string): Promise => { - let entries; - try { - entries = await NodeFSP.readdir(directory, { withFileTypes: true }); - } catch (error) { - if ((error as NodeJS.ErrnoException).code !== "ENOENT") errors.push(directory); - return; - } - entries.sort((a, b) => a.name.localeCompare(b.name)); - for (const entry of entries) { - const path = NodePath.join(directory, entry.name); - if (entry.isDirectory()) { - await walk(path, root); - } else if (entry.isFile() && entry.name.endsWith(".db")) { - try { - const canonical = await NodeFSP.realpath(path); - if (visited.has(canonical)) continue; - visited.add(canonical); - const stat = await NodeFSP.stat(path); - const wal = await NodeFSP.stat(`${path}-wal`).catch(() => null); - const fingerprint = [stat, wal] - .map((file) => (file ? `${file.size}:${file.mtimeMs}:${file.ctimeMs}` : "-")) - .join("/"); - const cached = cache?.get(canonical); - let candidates: readonly UsageCandidate[]; - if (cached?.fingerprint === fingerprint) { - candidates = cached.candidates; - } else { - candidates = await readDatabase(path, stat.mtimeMs); - cache?.set(canonical, { fingerprint, candidates }); - } + const readFile = Effect.fn("readAntigravityUsage.readFile")(function* (path: string) { + const canonical = yield* fileSystem.realPath(path); + if (visited.has(canonical)) return null; + visited.add(canonical); + const fingerprintOf = (file: string) => + Effect.tryPromise(() => NodeFSP.stat(file)).pipe( + Effect.map((stat) => ({ stat, part: `${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}` })), + ); + const { stat, part } = yield* fingerprintOf(path); + const wal = yield* fingerprintOf(`${path}-wal`).pipe(Effect.option); + const fingerprint = `${part}/${wal._tag === "Some" ? wal.value.part : "-"}`; + const cached = cache?.get(canonical); + if (cached?.fingerprint === fingerprint) return cached.candidates; + const candidates = yield* readDatabase( + path, + pathService.basename(path, ".db"), + stat.mtimeMs, + ).pipe(Effect.provide(NodeSqliteClient.layer({ filename: path, readonly: true }))); + cache?.set(canonical, { fingerprint, candidates }); + return candidates; + }); + const walk = (directory: string, root: string): Effect.Effect => + Effect.gen(function* () { + const names = yield* fileSystem.readDirectory(directory).pipe( + Effect.catchTags({ + PlatformError: (cause) => { + if (cause.reason._tag !== "NotFound") errors.push(directory); + return Effect.succeed(null); + }, + }), + ); + if (names === null) return; + names.sort((a, b) => a.localeCompare(b)); + for (const name of names) { + const path = pathService.join(directory, name); + // Like a directory listing's entry types: symlinks are neither files nor directories. + const isLink = Exit.isSuccess(yield* Effect.exit(fileSystem.readLink(path))); + const type = isLink + ? "SymbolicLink" + : yield* fileSystem.stat(path).pipe( + Effect.map((info) => info.type), + Effect.orElseSucceed(() => null), + ); + if (type === "Directory") { + yield* walk(path, root); + } else if ((type === "File" || type === null) && name.endsWith(".db")) { + const candidates = yield* readFile(path).pipe( + Effect.catch(() => { + errors.push(path); + return Effect.succeed(null); + }), + ); + if (candidates === null) continue; const fileIndex = files.length; files.push({ root, path, records: [] }); for (const [index, candidate] of candidates.entries()) { append(candidate, fileIndex); - if (index % 256 === 255) await NodeTimersPromises.setImmediate(); + if (index % 256 === 255) yield* Effect.yieldNow; } - } catch { - errors.push(path); } } - } - }; - for (const root of roots) await walk(root, root); + }); + for (const root of roots) yield* walk(root, root); if (cache !== undefined) { for (const key of cache.keys()) if (!visited.has(key)) cache.delete(key); } @@ -403,4 +460,4 @@ export async function readAntigravityUsage( } } return { files, errors }; -} +}); diff --git a/packages/provider-cursor/src/server/CursorAccountReader.ts b/packages/provider-cursor/src/server/CursorAccountReader.ts index a6922ef87a53..886bfe2f8737 100644 --- a/packages/provider-cursor/src/server/CursorAccountReader.ts +++ b/packages/provider-cursor/src/server/CursorAccountReader.ts @@ -5,9 +5,11 @@ * @module provider-cursor/server/CursorAccountReader */ import * as Context from "effect/Context"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as HttpClient from "effect/http/HttpClient"; import * as Layer from "effect/Layer"; -import * as Result from "effect/Result"; import type { CursorCredentialSource } from "./accountCache.ts"; import { readCursorAccountUsage, type CursorAccountUsageReadResult } from "./accountUsage.ts"; @@ -29,23 +31,16 @@ export const layer = Layer.effect( CursorAccountReader, Effect.gen(function* () { const keychain = yield* CursorKeychain.CursorKeychain; + const context = yield* Effect.context< + FileSystem.FileSystem | Crypto.Crypto | HttpClient.HttpClient + >(); return CursorAccountReader.of({ + // Only a Keychain login asks the Keychain. The dashboard reader turns + // its outcome, a failure included, into the source's message. read: (credentialSource, sinceMs, untilMs) => - Effect.gen(function* () { - // Only a Keychain login asks the Keychain. The dashboard reader - // turns its outcome, a failure included, into the source's message. - const token = - typeof credentialSource === "string" - ? Result.succeed(null) - : yield* Effect.result(keychain.accessToken); - return yield* Effect.promise(() => - readCursorAccountUsage(credentialSource, sinceMs, untilMs, globalThis.fetch, () => - Result.isSuccess(token) - ? Promise.resolve(token.success) - : Promise.reject(token.failure), - ), - ); - }), + readCursorAccountUsage(credentialSource, sinceMs, untilMs, keychain.accessToken).pipe( + Effect.provideContext(context), + ), }); }), ); diff --git a/packages/provider-cursor/src/server/accountUsage.test.ts b/packages/provider-cursor/src/server/accountUsage.test.ts index 32a0f41c0612..3924576b43dc 100644 --- a/packages/provider-cursor/src/server/accountUsage.test.ts +++ b/packages/provider-cursor/src/server/accountUsage.test.ts @@ -1,340 +1,411 @@ -// @effect-diagnostics nodeBuiltinImport:off - the reader under test reads a -// real CLI credential file from disk. -import * as NodeFSP from "node:fs/promises"; -import * as NodeOS from "node:os"; -import * as NodePath from "node:path"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, describe, it } from "@effect/vitest"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as FetchHttpClient from "effect/http/FetchHttpClient"; +import * as HttpClient from "effect/http/HttpClient"; +import * as HttpClientError from "effect/http/HttpClientError"; +import * as HttpClientRequest from "effect/http/HttpClientRequest"; +import * as HttpClientResponse from "effect/http/HttpClientResponse"; +import * as Path from "effect/Path"; -import { afterEach, assert, beforeEach, describe, it } from "@effect/vitest"; - -import { CursorKeychainTimeoutError } from "./CursorKeychain.ts"; +import * as CursorKeychain from "./CursorKeychain.ts"; import { readCursorAccountUsage } from "./accountUsage.ts"; -let dir: string; - -beforeEach(async () => { - dir = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "usage-reader-test-")); -}); +/** A scoped temporary directory holding `auth.json` with `accessToken`. */ +const writeAuth = (accessToken: string) => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "usage-reader-test-" }); + const authPath = path.join(dir, "auth.json"); + yield* fileSystem.writeFileString(authPath, JSON.stringify({ accessToken })); + return { dir, authPath }; + }); -afterEach(async () => { - await NodeFSP.rm(dir, { recursive: true, force: true }); -}); +/** One dashboard request as Cursor sees it: the URL, the fetch options and the decoded page. */ +interface DashboardRequest { + readonly url: string; + readonly init: RequestInit; + readonly page: number; + readonly headers: Headers; +} -describe("Cursor account history", () => { - it("reads Cursor account history with the default macOS Keychain login", async () => { - const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; - let keychainReads = 0; - const result = await readCursorAccountUsage( - { kind: "keychain" }, - 0, - 1781000000000, - async (_url, init) => { - assert.include(new Headers(init.headers).get("cookie") ?? "", "demo%3A%3A"); - return Response.json({ totalUsageEventsCount: 0, usageEventsDisplay: [] }); - }, - async () => { - keychainReads++; - return accessToken; +/** Stands in for Cursor's dashboard: `answer` replies to each page request. */ +const dashboard = (answer: (request: DashboardRequest) => Response | Promise) => + HttpClient.make((request, url, signal, fiber) => + Effect.tryPromise({ + try: async () => { + const init = Context.getOrUndefined(fiber.context, FetchHttpClient.RequestInit); + const body = + request.body._tag === "Uint8Array" ? new TextDecoder().decode(request.body.body) : ""; + const response = await answer({ + url: url.toString(), + init: { ...init, signal, body }, + page: JSON.parse(body).page, + headers: new Headers(request.headers), + }); + return HttpClientResponse.fromWeb(request, response); }, - ); - assert.strictEqual(keychainReads, 1); - assert.isNull(result.error); - assert.isFalse(result.missing); - assert.isNotNull(result.accountKey); - }); + catch: (cause) => + new HttpClientError.HttpClientError({ + reason: new HttpClientError.TransportError({ request, cause }), + }), + }), + ); - it("reads paginated Cursor account history including headless calls with separate cache tokens", async () => { - const authPath = NodePath.join(dir, "auth.json"); - const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo", exp: 4102444800 })).toString("base64url")}.signature`; - await NodeFSP.writeFile(authPath, JSON.stringify({ accessToken })); - const pages: number[] = []; - const signals: AbortSignal[] = []; - const request = async (url: string, init: RequestInit) => { - assert.strictEqual(String(url), "https://cursor.com/api/dashboard/get-filtered-usage-events"); - assert.strictEqual(init?.redirect, "error"); - const headers = new Headers(init?.headers); - assert.strictEqual(headers.get("origin"), "https://cursor.com"); - assert.include(headers.get("cookie") ?? "", "WorkosCursorSessionToken=demo%3A%3A"); - const body = JSON.parse(String(init?.body)); - pages.push(body.page); - if (init.signal) signals.push(init.signal); - return Response.json({ - totalUsageEventsCount: 1001, - usageEventsDisplay: Array.from({ length: body.page === 1 ? 1000 : 1 }, (_, index) => ({ - timestamp: String(1780000000000 + ((body.page - 1) * 1000 + index) * 1000), - model: "claude-sonnet-4-5", - conversationId: `conversation-${body.page}`, - isHeadless: body.page === 2, - chargedCents: 0, - tokenUsage: { - inputTokens: 10, - outputTokens: 5, - cacheReadTokens: 30, - cacheWriteTokens: 2, - totalCents: 25, - }, - })), - }); - }; - const result = await readCursorAccountUsage(authPath, 0, 1781000000000, request); - assert.isNull(result.error); - assert.deepStrictEqual(pages, [1, 2]); - assert.lengthOf(signals, 2); - assert.notStrictEqual(signals[0], signals[1]); - assert.strictEqual(result.records.length, 1001); - assert.strictEqual(result.records.at(-1)?.sessionId, "conversation-2"); - assert.deepStrictEqual(result.records[0]?.totals, { - uncachedInputTokens: 10, - cachedInputTokens: 30, - cacheCreationTokens: 2, - outputTokens: 5, - reasoningTokens: 0, - }); - assert.strictEqual(result.records[0]?.reportedCostUsd, 0.25); - assert.isFalse(result.accountKey?.includes("demo") ?? true); - }); - - it("reads Cursor account history beyond 100 pages", async () => { - const authPath = NodePath.join(dir, "auth.json"); - const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; - await NodeFSP.writeFile(authPath, JSON.stringify({ accessToken })); - const fullPage = Array.from({ length: 1000 }, () => ({ tokenUsage: null })); - let requests = 0; - const result = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => { - requests += 1; - return Response.json({ - totalUsageEventsCount: 100_001, - usageEventsDisplay: requests <= 100 ? fullPage : [{ tokenUsage: null }], - }); - }); - assert.isNull(result.error); - assert.strictEqual(requests, 101); - assert.deepStrictEqual(result.records, []); - }); +/** Runs `readCursorAccountUsage` against `answer` standing in for Cursor. */ +const readAccountUsage = ( + credentialSource: Parameters[0], + sinceMs: number, + endDate: number, + answer: (request: DashboardRequest) => Response | Promise, + keychainToken?: Parameters[3], +) => + readCursorAccountUsage(credentialSource, sinceMs, endDate, keychainToken).pipe( + Effect.provideService(HttpClient.HttpClient, dashboard(answer)), + ); - it("accepts confirmed empty Cursor usage but rejects error envelopes", async () => { - const authPath = NodePath.join(dir, "auth.json"); - const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; - await NodeFSP.writeFile(authPath, JSON.stringify({ accessToken })); - for (const body of [ - {}, - { totalUsageEventsCount: 0 }, - { totalUsageEventsCount: 0, usageEventsDisplay: [] }, - ]) { - const result = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => - Response.json(body), +describe("Cursor account history", () => { + it.effect("reads Cursor account history with the default macOS Keychain login", () => + Effect.gen(function* () { + const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; + let keychainReads = 0; + const result = yield* readAccountUsage( + { kind: "keychain" }, + 0, + 1781000000000, + ({ headers }) => { + assert.include(headers.get("cookie") ?? "", "demo%3A%3A"); + return Response.json({ totalUsageEventsCount: 0, usageEventsDisplay: [] }); + }, + Effect.sync(() => { + keychainReads++; + return accessToken; + }), ); + assert.strictEqual(keychainReads, 1); assert.isNull(result.error); - assert.deepStrictEqual(result.records, []); assert.isFalse(result.missing); - } - for (const body of [ - { error: "upstream error" }, - { detail: "unknown error envelope" }, - { totalUsageEventsCount: 0, error: "upstream error" }, - null, - [], - "invalid", - 0, - ]) { - const result = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => - Response.json(body), - ); - assert.isNotNull(result.error); - assert.deepStrictEqual(result.records, []); - } - }); + assert.isNotNull(result.accountKey); + }).pipe(Effect.provide(NodeServices.layer)), + ); - it("requires a terminal Cursor page after a full page reaches the reported count", async () => { - const authPath = NodePath.join(dir, "auth.json"); - const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; - await NodeFSP.writeFile(authPath, JSON.stringify({ accessToken })); - let requests = 0; - const result = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => { - requests++; - return Response.json( - requests === 1 - ? { - totalUsageEventsCount: 1000, - usageEventsDisplay: Array.from({ length: 1000 }, (_, index) => ({ - timestamp: String(1780000000000 + index), - model: "gpt-5", - tokenUsage: { inputTokens: 10, outputTokens: 5 }, - })), - } - : { totalUsageEventsCount: 1000 }, - ); - }); - assert.isNull(result.error); - assert.strictEqual(result.records.length, 1000); - assert.strictEqual(requests, 2); - }); + it.effect( + "reads paginated Cursor account history including headless calls with separate cache tokens", + () => + Effect.gen(function* () { + const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo", exp: 4102444800 })).toString("base64url")}.signature`; + const { authPath } = yield* writeAuth(accessToken); + const pages: number[] = []; + const signals: AbortSignal[] = []; + const request = ({ url, init, headers, page }: DashboardRequest) => { + assert.strictEqual(url, "https://cursor.com/api/dashboard/get-filtered-usage-events"); + assert.strictEqual(init.redirect, "error"); + assert.strictEqual(headers.get("origin"), "https://cursor.com"); + assert.strictEqual(headers.get("content-type"), "application/json"); + assert.include(headers.get("cookie") ?? "", "WorkosCursorSessionToken=demo%3A%3A"); + const body = { page }; + pages.push(body.page); + if (init.signal) signals.push(init.signal); + return Response.json({ + totalUsageEventsCount: 1001, + usageEventsDisplay: Array.from({ length: body.page === 1 ? 1000 : 1 }, (_, index) => ({ + timestamp: String(1780000000000 + ((body.page - 1) * 1000 + index) * 1000), + model: "claude-sonnet-4-5", + conversationId: `conversation-${body.page}`, + isHeadless: body.page === 2, + chargedCents: 0, + tokenUsage: { + inputTokens: 10, + outputTokens: 5, + cacheReadTokens: 30, + cacheWriteTokens: 2, + totalCents: 25, + }, + })), + }); + }; + const result = yield* readAccountUsage(authPath, 0, 1781000000000, request); + assert.isNull(result.error); + assert.deepStrictEqual(pages, [1, 2]); + assert.lengthOf(signals, 2); + assert.notStrictEqual(signals[0], signals[1]); + assert.strictEqual(result.records.length, 1001); + assert.strictEqual(result.records.at(-1)?.sessionId, "conversation-2"); + assert.deepStrictEqual(result.records[0]?.totals, { + uncachedInputTokens: 10, + cachedInputTokens: 30, + cacheCreationTokens: 2, + outputTokens: 5, + reasoningTokens: 0, + }); + assert.strictEqual(result.records[0]?.reportedCostUsd, 0.25); + assert.isFalse(result.accountKey?.includes("demo") ?? true); + }).pipe(Effect.provide(NodeServices.layer)), + ); - it("removes only count-proven Cursor boundary copies and preserves identical billed events", async () => { - const authPath = NodePath.join(dir, "auth.json"); - const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; - await NodeFSP.writeFile(authPath, JSON.stringify({ accessToken })); - const event = (index: number) => ({ - timestamp: String(1780000000000 + index), - model: "gpt-5", - tokenUsage: { inputTokens: 10, outputTokens: 5, totalCents: 1 }, - }); - for (const total of [2000, 2001]) { + it.effect("reads Cursor account history beyond 100 pages", () => + Effect.gen(function* () { + const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; + const { authPath } = yield* writeAuth(accessToken); + const fullPage = Array.from({ length: 1000 }, () => ({ tokenUsage: null })); let requests = 0; - const result = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => { - requests++; + const result = yield* readAccountUsage(authPath, 0, 1781000000000, async () => { + requests += 1; return Response.json({ - totalUsageEventsCount: total, - usageEventsDisplay: - requests === 1 - ? Array.from({ length: 1000 }, (_, index) => event(index)) - : requests === 2 - ? Array.from({ length: 1000 }, (_, index) => event(999 + index)) - : [event(1999)], + totalUsageEventsCount: 100_001, + usageEventsDisplay: requests <= 100 ? fullPage : [{ tokenUsage: null }], }); }); assert.isNull(result.error); - assert.strictEqual(result.records.length, total); - assert.strictEqual(requests, 3); - assert.strictEqual(result.records.at(-1)?.timestampMs, 1780000001999); - assert.strictEqual( - result.records.filter((record) => record.timestampMs === 1780000000999).length, - total === 2000 ? 1 : 2, - ); - assert.strictEqual(new Set(result.records.map((record) => record.dedupeKey)).size, total); - } - let requests = 0; - const inconsistent = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => { - requests++; - return Response.json({ - totalUsageEventsCount: 1001, - usageEventsDisplay: + assert.strictEqual(requests, 101); + assert.deepStrictEqual(result.records, []); + }).pipe(Effect.provide(NodeServices.layer)), + ); + + it.effect("accepts confirmed empty Cursor usage but rejects error envelopes", () => + Effect.gen(function* () { + const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; + const { authPath } = yield* writeAuth(accessToken); + for (const body of [ + {}, + { totalUsageEventsCount: 0 }, + { totalUsageEventsCount: 0, usageEventsDisplay: [] }, + ]) { + const result = yield* readAccountUsage(authPath, 0, 1781000000000, async () => + Response.json(body), + ); + assert.isNull(result.error); + assert.deepStrictEqual(result.records, []); + assert.isFalse(result.missing); + } + for (const body of [ + { error: "upstream error" }, + { detail: "unknown error envelope" }, + { totalUsageEventsCount: 0, error: "upstream error" }, + null, + [], + "invalid", + 0, + ]) { + const result = yield* readAccountUsage(authPath, 0, 1781000000000, async () => + Response.json(body), + ); + assert.isNotNull(result.error); + assert.deepStrictEqual(result.records, []); + } + }).pipe(Effect.provide(NodeServices.layer)), + ); + + it.effect("requires a terminal Cursor page after a full page reaches the reported count", () => + Effect.gen(function* () { + const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; + const { authPath } = yield* writeAuth(accessToken); + let requests = 0; + const result = yield* readAccountUsage(authPath, 0, 1781000000000, async () => { + requests++; + return Response.json( requests === 1 - ? Array.from({ length: 1000 }, (_, index) => event(index)) - : [event(500), event(1000)], + ? { + totalUsageEventsCount: 1000, + usageEventsDisplay: Array.from({ length: 1000 }, (_, index) => ({ + timestamp: String(1780000000000 + index), + model: "gpt-5", + tokenUsage: { inputTokens: 10, outputTokens: 5 }, + })), + } + : { totalUsageEventsCount: 1000 }, + ); }); - }); - assert.isNotNull(inconsistent.error); - assert.deepStrictEqual(inconsistent.records, []); - }); + assert.isNull(result.error); + assert.strictEqual(result.records.length, 1000); + assert.strictEqual(requests, 2); + }).pipe(Effect.provide(NodeServices.layer)), + ); - it("does not present truncated Cursor account pages or authentication failures as complete history", async () => { - const authPath = NodePath.join(dir, "auth.json"); - const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo", exp: 4102444800 })).toString("base64url")}.signature`; - await NodeFSP.writeFile(authPath, JSON.stringify({ accessToken })); - const truncated = await readCursorAccountUsage(authPath, 0, 1781000000000, async () => - Response.json({ totalUsageEventsCount: 101, usageEventsDisplay: [] }), - ); - assert.isNotNull(truncated.error); - assert.deepStrictEqual(truncated.records, []); - const denied = await readCursorAccountUsage( - authPath, - 0, - 1781000000000, - async () => new Response(accessToken, { status: 401 }), - ); - assert.isNotNull(denied.error); - assert.isFalse(denied.error?.includes(accessToken) ?? true); - assert.deepStrictEqual(denied.records, []); - let requested = false; - const missing = await readCursorAccountUsage( - NodePath.join(dir, "missing.json"), - 0, - 1781000000000, - async () => { - requested = true; - return Response.json({}); - }, - ); - assert.isTrue(missing.missing); - assert.isFalse(requested); - }); + it.effect( + "removes only count-proven Cursor boundary copies and preserves identical billed events", + () => + Effect.gen(function* () { + const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; + const { authPath } = yield* writeAuth(accessToken); + const event = (index: number) => ({ + timestamp: String(1780000000000 + index), + model: "gpt-5", + tokenUsage: { inputTokens: 10, outputTokens: 5, totalCents: 1 }, + }); + for (const total of [2000, 2001]) { + let requests = 0; + const result = yield* readAccountUsage(authPath, 0, 1781000000000, async () => { + requests++; + return Response.json({ + totalUsageEventsCount: total, + usageEventsDisplay: + requests === 1 + ? Array.from({ length: 1000 }, (_, index) => event(index)) + : requests === 2 + ? Array.from({ length: 1000 }, (_, index) => event(999 + index)) + : [event(1999)], + }); + }); + assert.isNull(result.error); + assert.strictEqual(result.records.length, total); + assert.strictEqual(requests, 3); + assert.strictEqual(result.records.at(-1)?.timestampMs, 1780000001999); + assert.strictEqual( + result.records.filter((record) => record.timestampMs === 1780000000999).length, + total === 2000 ? 1 : 2, + ); + assert.strictEqual(new Set(result.records.map((record) => record.dedupeKey)).size, total); + } + let requests = 0; + const inconsistent = yield* readAccountUsage(authPath, 0, 1781000000000, async () => { + requests++; + return Response.json({ + totalUsageEventsCount: 1001, + usageEventsDisplay: + requests === 1 + ? Array.from({ length: 1000 }, (_, index) => event(index)) + : [event(500), event(1000)], + }); + }); + assert.isNotNull(inconsistent.error); + assert.deepStrictEqual(inconsistent.records, []); + }).pipe(Effect.provide(NodeServices.layer)), + ); + + it.effect( + "does not present truncated Cursor account pages or authentication failures as complete history", + () => + Effect.gen(function* () { + const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo", exp: 4102444800 })).toString("base64url")}.signature`; + const { dir, authPath } = yield* writeAuth(accessToken); + const truncated = yield* readAccountUsage(authPath, 0, 1781000000000, async () => + Response.json({ totalUsageEventsCount: 101, usageEventsDisplay: [] }), + ); + assert.isNotNull(truncated.error); + assert.deepStrictEqual(truncated.records, []); + const denied = yield* readAccountUsage( + authPath, + 0, + 1781000000000, + async () => new Response(accessToken, { status: 401 }), + ); + assert.isNotNull(denied.error); + assert.isFalse(denied.error?.includes(accessToken) ?? true); + assert.deepStrictEqual(denied.records, []); + let requested = false; + const missing = yield* readAccountUsage( + (yield* Path.Path).join(dir, "missing.json"), + 0, + 1781000000000, + async () => { + requested = true; + return Response.json({}); + }, + ); + assert.isTrue(missing.missing); + assert.isFalse(requested); + }).pipe(Effect.provide(NodeServices.layer)), + ); }); describe("readCursorAccountUsage", () => { - it("asks for Keychain approval when the prompt goes unanswered", async () => { - const result = await readCursorAccountUsage( - { kind: "keychain" }, - 0, - 1, - () => Promise.reject(new Error("no network expected")), - () => Promise.reject(new CursorKeychainTimeoutError()), - ); - assert.deepStrictEqual(result, { - accountKey: null, - records: [], - missing: false, - error: "Allow Keychain access on the Mac running T3 Code, then refresh.", - }); - }); - - it("reads the pages behind the first together and keeps them in page order", async () => { - const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; - let inFlight = 0; - let mostInFlight = 0; - const result = await readCursorAccountUsage( - { kind: "keychain" }, - 0, - 1781000000000, - async (_url, init) => { - const page: number = JSON.parse(String(init.body)).page; - inFlight++; - mostInFlight = Math.max(mostInFlight, inFlight); - // Later pages answer first, so record order cannot come from arrival order. - for (let turn = page; turn < 9; turn++) await Promise.resolve(); - inFlight--; - return Response.json({ - totalUsageEventsCount: 8001, - usageEventsDisplay: Array.from({ length: page === 9 ? 1 : 1000 }, (_, index) => ({ - timestamp: String(1780000000000 + (page - 1) * 1000 + index), - model: "gpt-5", - tokenUsage: { inputTokens: 1 }, - })), - }); - }, - async () => accessToken, - ); - assert.isNull(result.error); - assert.strictEqual(mostInFlight, 6); - assert.deepStrictEqual( - result.records.map((record) => record.timestampMs), - Array.from({ length: 8001 }, (_, index) => 1780000000000 + index), - ); - }); + it.effect("asks for Keychain approval when the prompt goes unanswered", () => + Effect.gen(function* () { + const result = yield* readAccountUsage( + { kind: "keychain" }, + 0, + 1, + () => { + throw new Error("no network expected"); + }, + Effect.fail(new CursorKeychain.CursorKeychainTimeoutError()), + ); + assert.deepStrictEqual(result, { + accountKey: null, + records: [], + missing: false, + error: "Allow Keychain access on the Mac running T3 Code, then refresh.", + }); + }).pipe(Effect.provide(NodeServices.layer)), + ); - it("ends like a page-by-page read when a page fails, leaving no request open", async () => { - const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; - const fullPage = { - totalUsageEventsCount: 8001, - usageEventsDisplay: Array.from({ length: 1000 }, () => ({ tokenUsage: null })), - }; - // Page 3 fails while the pages after it are still waiting on Cursor. - for (const [secondPage, error] of [ - [() => Response.json(fullPage), "Cursor account usage could not be read."], - [() => new Response(null, { status: 401 }), "Sign in to Cursor again to read account usage."], - ] as const) { - let open = 0; - const result = await readCursorAccountUsage( + it.effect("reads the pages behind the first together and keeps them in page order", () => + Effect.gen(function* () { + const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; + let inFlight = 0; + let mostInFlight = 0; + const result = yield* readAccountUsage( { kind: "keychain" }, 0, 1781000000000, - (_url, init) => { - const page: number = JSON.parse(String(init.body)).page; - if (page === 1) return Promise.resolve(Response.json(fullPage)); - if (page === 2) return Promise.resolve(secondPage()); - if (page === 3) return Promise.reject(new Error("connection reset")); - open++; - return new Promise((_resolve, reject) => { - init.signal?.addEventListener("abort", () => { - open--; - reject(init.signal?.reason); - }); + async ({ page }) => { + inFlight++; + mostInFlight = Math.max(mostInFlight, inFlight); + // Later pages answer first, so record order cannot come from arrival order. + for (let turn = page; turn < 9; turn++) await Promise.resolve(); + inFlight--; + return Response.json({ + totalUsageEventsCount: 8001, + usageEventsDisplay: Array.from({ length: page === 9 ? 1 : 1000 }, (_, index) => ({ + timestamp: String(1780000000000 + (page - 1) * 1000 + index), + model: "gpt-5", + tokenUsage: { inputTokens: 1 }, + })), }); }, - async () => accessToken, + Effect.succeed(accessToken), ); - assert.strictEqual(result.error, error); - assert.deepStrictEqual(result.records, []); - assert.strictEqual(open, 0); - } - }); + assert.isNull(result.error); + assert.strictEqual(mostInFlight, 6); + assert.deepStrictEqual( + result.records.map((record) => record.timestampMs), + Array.from({ length: 8001 }, (_, index) => 1780000000000 + index), + ); + }).pipe(Effect.provide(NodeServices.layer)), + ); + + it.effect("ends like a page-by-page read when a page fails, leaving no request open", () => + Effect.gen(function* () { + const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`; + const fullPage = { + totalUsageEventsCount: 8001, + usageEventsDisplay: Array.from({ length: 1000 }, () => ({ tokenUsage: null })), + }; + // Page 3 fails while the pages after it are still waiting on Cursor. + for (const [secondPage, error] of [ + [() => Response.json(fullPage), "Cursor account usage could not be read."], + [ + () => new Response(null, { status: 401 }), + "Sign in to Cursor again to read account usage.", + ], + ] as const) { + let open = 0; + const result = yield* readAccountUsage( + { kind: "keychain" }, + 0, + 1781000000000, + ({ init, page }) => { + if (page === 1) return Promise.resolve(Response.json(fullPage)); + if (page === 2) return Promise.resolve(secondPage()); + if (page === 3) return Promise.reject(new Error("connection reset")); + open++; + return new Promise((_resolve, reject) => { + init.signal?.addEventListener("abort", () => { + open--; + reject(init.signal?.reason); + }); + }); + }, + Effect.succeed(accessToken), + ); + assert.strictEqual(result.error, error); + assert.deepStrictEqual(result.records, []); + assert.strictEqual(open, 0); + } + }).pipe(Effect.provide(NodeServices.layer)), + ); }); diff --git a/packages/provider-cursor/src/server/accountUsage.ts b/packages/provider-cursor/src/server/accountUsage.ts index 4ecab1ace876..11976a2e0f1d 100644 --- a/packages/provider-cursor/src/server/accountUsage.ts +++ b/packages/provider-cursor/src/server/accountUsage.ts @@ -1,5 +1,3 @@ -// Node fs reads CLI credentials, and crypto hashes account IDs for deduplication. -// @effect-diagnostics nodeBuiltinImport:off /** * Cursor account usage from its dashboard API, read with the saved CLI or * Keychain login. The dashboard covers CLI, desktop and headless usage from @@ -7,13 +5,22 @@ * * @module provider-cursor/server/accountUsage */ -import * as NodeFSP from "node:fs/promises"; -import * as NodeCrypto from "node:crypto"; -import * as NodeTimersPromises from "node:timers/promises"; - import type { UsageRecord } from "@t3tools/provider-core/server/usage"; +import * as Crypto from "effect/Crypto"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Base64Url from "effect/encoding/Base64Url"; +import * as Hex from "effect/encoding/Hex"; +import * as Fiber from "effect/Fiber"; +import * as FileSystem from "effect/FileSystem"; +import * as FetchHttpClient from "effect/http/FetchHttpClient"; +import * as HttpClient from "effect/http/HttpClient"; +import * as HttpClientRequest from "effect/http/HttpClientRequest"; +import * as PlatformError from "effect/PlatformError"; +import * as Result from "effect/Result"; +import * as Schema from "effect/Schema"; -import { isCursorKeychainTimeoutError } from "./CursorKeychain.ts"; +import * as CursorKeychain from "./CursorKeychain.ts"; function object(value: unknown): Record { return typeof value === "object" && value !== null && !Array.isArray(value) @@ -44,7 +51,27 @@ export interface CursorAccountUsageReadResult { readonly error: string | null; } -const accountHash = (value: string) => NodeCrypto.createHash("sha256").update(value).digest("hex"); +/** Why a dashboard read was abandoned; the caller only sees the generic message. */ +class CursorAccountUsageInvalidError extends Schema.TaggedError()( + "CursorAccountUsageInvalidError", + { reason: Schema.String }, +) {} + +const invalid = (reason: string) => Effect.fail(new CursorAccountUsageInvalidError({ reason })); + +const decodeJson = Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown)); + +const pageRequestBody = (page: number, pageSize: number, sinceMs: number, endDate: number) => + JSON.stringify({ page, pageSize, startDate: String(sinceMs), endDate: String(endDate) }); + +const billedEventIdentity = (...fields: readonly unknown[]) => JSON.stringify(fields); + +const textEncoder = new TextEncoder(); + +const accountHash = Effect.fn("accountHash")(function* (value: string) { + const crypto = yield* Crypto.Crypto; + return Hex.encode(yield* crypto.digest("SHA-256", textEncoder.encode(value))); +}); function canonicalJson(value: unknown): string { if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; @@ -70,26 +97,54 @@ function boundaryOverlap(previous: readonly string[], current: readonly string[] return lengths.at(-1) ?? 0; } +const DASHBOARD_USAGE_URL = "https://cursor.com/api/dashboard/get-filtered-usage-events"; + +/** The session cookie must never follow a redirect off cursor.com. */ +const NO_REDIRECT: RequestInit = { redirect: "error" }; + /** * Dashboard usage includes headless agents and reports fresh input separately - * from cache reads. `keychainToken` is only called for a Keychain login. + * from cache reads. `keychainToken` is only run for a Keychain login. */ -export async function readCursorAccountUsage( +export const readCursorAccountUsage = Effect.fn("readCursorAccountUsage")(function* ( credentialSource: string | { readonly kind: "keychain" }, sinceMs: number, endDate: number, - request: (url: string, init: RequestInit) => Promise = globalThis.fetch, - keychainToken: () => Promise = () => - Promise.reject(new Error("No Keychain reader")), -): Promise { - let accessToken: unknown; - try { - accessToken = - typeof credentialSource === "string" - ? object(JSON.parse(await NodeFSP.readFile(credentialSource, "utf8"))).accessToken - : await keychainToken(); - } catch (cause) { - const missing = typeof credentialSource === "string" && object(cause).code === "ENOENT"; + keychainToken: Effect.Effect< + string | null, + | CursorKeychain.CursorKeychainTimeoutError + | CursorKeychain.CursorKeychainReadError + | CursorAccountUsageInvalidError + > = invalid("No Keychain reader"), +): Effect.fn.Return< + CursorAccountUsageReadResult, + never, + FileSystem.FileSystem | Crypto.Crypto | HttpClient.HttpClient +> { + const fileSystem = yield* FileSystem.FileSystem; + const httpClient = yield* HttpClient.HttpClient; + const credential = yield* Effect.result< + unknown, + | PlatformError.PlatformError + | Schema.SchemaError + | CursorKeychain.CursorKeychainTimeoutError + | CursorKeychain.CursorKeychainReadError + | CursorAccountUsageInvalidError, + never + >( + typeof credentialSource === "string" + ? fileSystem.readFileString(credentialSource).pipe( + Effect.flatMap(decodeJson), + Effect.map((credentials) => object(credentials).accessToken), + ) + : keychainToken, + ); + if (Result.isFailure(credential)) { + const cause = credential.failure; + const missing = + typeof credentialSource === "string" && + PlatformError.isPlatformError(cause) && + cause.reason._tag === "NotFound"; return { accountKey: null, records: [], @@ -98,11 +153,12 @@ export async function readCursorAccountUsage( ? null : typeof credentialSource === "string" ? "Cursor credentials could not be read." - : isCursorKeychainTimeoutError(cause) + : CursorKeychain.isCursorKeychainTimeoutError(cause) ? "Allow Keychain access on the Mac running T3 Code, then refresh." : "Cursor Keychain credentials could not be read.", }; } + const accessToken = credential.success; if (typeof accessToken !== "string" || !accessToken) { return { accountKey: null, @@ -115,95 +171,112 @@ export async function readCursorAccountUsage( }; } let accountKey: string | null = null; - const cancel = new AbortController(); - try { - const payload = accessToken.split(".")[1]; + const read = Effect.gen(function* () { + const payload = Base64Url.decodeString(accessToken.split(".")[1] ?? ""); + if (Result.isFailure(payload)) return yield* invalid("Invalid authentication"); const subject = object( - JSON.parse(Buffer.from(payload ?? "", "base64url").toString("utf8")), + yield* decodeJson(payload.success).pipe( + Effect.catch(() => invalid("Invalid authentication")), + ), ).sub; - if (typeof subject !== "string" || !subject) throw new Error("Invalid authentication"); + if (typeof subject !== "string" || !subject) return yield* invalid("Invalid authentication"); const userId = subject.split("|").at(-1); - if (!userId) throw new Error("Invalid authentication"); - accountKey = accountHash(subject); + if (!userId) return yield* invalid("Invalid authentication"); + accountKey = yield* accountHash(subject); if (!Number.isFinite(sinceMs) || !Number.isFinite(endDate) || sinceMs < 0 || sinceMs > endDate) - throw new Error("Invalid date window"); - const deadline = AbortSignal.any([cancel.signal, AbortSignal.timeout(60_000)]); + return yield* invalid("Invalid date window"); const records: UsageRecord[] = []; const occurrences = new Map(); const pages: unknown[][] = []; let completed = false; const pageSize = 1000; let total: number | undefined; - const readPage = async (page: number) => { - // A count can include overlapping page boundaries. Allow room to - // reconcile them without imposing a fixed account-size limit. - if (page > (total === undefined ? 1000 : Math.ceil(total / pageSize) * 2 + 1)) { - throw new Error("Account usage page limit exceeded"); - } - const response = await request("https://cursor.com/api/dashboard/get-filtered-usage-events", { - method: "POST", - redirect: "error", - signal: AbortSignal.any([deadline, AbortSignal.timeout(10_000)]), - headers: { - "Content-Type": "application/json", - Origin: "https://cursor.com", - Cookie: `WorkosCursorSessionToken=${encodeURIComponent(`${userId}::${accessToken}`)}`, - }, - body: JSON.stringify({ - page, - pageSize, - startDate: String(sinceMs), - endDate: String(endDate), + const readPage = (page: number) => + Effect.gen(function* () { + // A count can include overlapping page boundaries. Allow room to + // reconcile them without imposing a fixed account-size limit. + if (page > (total === undefined ? 1000 : Math.ceil(total / pageSize) * 2 + 1)) { + return yield* invalid("Account usage page limit exceeded"); + } + const response = yield* httpClient + .execute( + HttpClientRequest.post(DASHBOARD_USAGE_URL).pipe( + HttpClientRequest.setHeaders({ + Origin: "https://cursor.com", + Cookie: `WorkosCursorSessionToken=${encodeURIComponent(`${userId}::${accessToken}`)}`, + }), + HttpClientRequest.bodyText( + pageRequestBody(page, pageSize, sinceMs, endDate), + "application/json", + ), + ), + ) + .pipe( + Effect.provideService(FetchHttpClient.RequestInit, NO_REDIRECT), + Effect.mapError( + () => new CursorAccountUsageInvalidError({ reason: "Account usage request failed" }), + ), + ); + if (response.status === 401 || response.status === 403) { + return { + accountKey, + records: [], + missing: false, + error: "Sign in to Cursor again to read account usage.", + } satisfies CursorAccountUsageReadResult; + } + if (response.status < 200 || response.status >= 300) + return yield* invalid("Account usage request failed"); + const parsed: unknown = yield* response.json.pipe( + Effect.mapError( + () => new CursorAccountUsageInvalidError({ reason: "Invalid account usage page" }), + ), + ); + if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + return yield* invalid("Invalid account usage page"); + } + const body = object(parsed); + const keys = Object.keys(body); + if ("error" in body || "message" in body || "code" in body) + return yield* invalid("Account usage error response"); + const count = keys.length === 0 ? 0 : body.totalUsageEventsCount; + const events = + keys.length === 0 || (keys.length === 1 && keys[0] === "totalUsageEventsCount") + ? [] + : body.usageEventsDisplay; + if ( + (count !== undefined && + (typeof count !== "number" || + !Number.isSafeInteger(count) || + count < 0 || + (total !== undefined && count !== total))) || + !Array.isArray(events) || + events.length > pageSize || + (count === undefined && !Array.isArray(body.usageEventsDisplay)) + ) { + return yield* invalid("Inconsistent account usage page"); + } + if (typeof count === "number") total = count; + return events as unknown[]; + }).pipe( + Effect.timeoutOrElse({ + duration: Duration.seconds(10), + orElse: () => invalid("Account usage request timed out"), }), - }); - if (response.status === 401 || response.status === 403) { - return { - accountKey, - records: [], - missing: false, - error: "Sign in to Cursor again to read account usage.", - }; - } - if (!response.ok) throw new Error("Account usage request failed"); - const parsed: unknown = await response.json(); - if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { - throw new Error("Invalid account usage page"); - } - const body = object(parsed); - const keys = Object.keys(body); - if ("error" in body || "message" in body || "code" in body) - throw new Error("Account usage error response"); - const count = keys.length === 0 ? 0 : body.totalUsageEventsCount; - const events = - keys.length === 0 || (keys.length === 1 && keys[0] === "totalUsageEventsCount") - ? [] - : body.usageEventsDisplay; - if ( - (count !== undefined && - (typeof count !== "number" || - !Number.isSafeInteger(count) || - count < 0 || - (total !== undefined && count !== total))) || - !Array.isArray(events) || - events.length > pageSize || - (count === undefined && !Array.isArray(body.usageEventsDisplay)) - ) { - throw new Error("Inconsistent account usage page"); - } - if (typeof count === "number") total = count; - return events; - }; + ); // Cursor caps a page at 1,000 events and takes about a second to answer one, so the pages // the first one's count implies are requested up to six ahead and consumed in page order. - const ahead: ReturnType[] = []; + // Pages still in flight when the read ends are interrupted with its scope. + const ahead: Fiber.Fiber< + Effect.Success>, + CursorAccountUsageInvalidError + >[] = []; for (let page = 1; ; page++) { while (ahead.length < Math.min(6, Math.ceil((total ?? 0) / pageSize) - page + 1)) { - const pending = readPage(page + ahead.length); - // Handled when its turn comes, or cancelled if the read ends first. - pending.catch(() => undefined); - ahead.push(pending); + ahead.push(yield* Effect.forkScoped(readPage(page + ahead.length))); } - const events = await (ahead.shift() ?? readPage(page)); + const next = ahead.shift(); + const events = yield* next === undefined ? readPage(page) : Fiber.join(next); // A rejected login comes back as the finished result instead of a page. if (!Array.isArray(events)) return events; pages.push(events); @@ -212,14 +285,17 @@ export async function readCursorAccountUsage( break; } } - if (!completed) throw new Error("Account usage page limit exceeded"); + if (!completed) return yield* invalid("Account usage page limit exceeded"); const rawCount = pages.reduce((sum, page) => sum + page.length, 0); - if (total !== undefined && rawCount < total) throw new Error("Incomplete account usage pages"); + if (total !== undefined && rawCount < total) + return yield* invalid("Incomplete account usage pages"); let removalsRemaining = total === undefined ? 0 : rawCount - total; let previousKeys: string[] = []; for (const events of pages) { const eventKeys = - removalsRemaining > 0 ? events.map((event) => accountHash(canonicalJson(event))) : []; + removalsRemaining > 0 + ? yield* Effect.forEach(events, (event) => accountHash(canonicalJson(event))) + : []; const removalCount = Math.min(removalsRemaining, boundaryOverlap(previousKeys, eventKeys)); removalsRemaining -= removalCount; previousKeys = eventKeys; @@ -239,7 +315,7 @@ export async function readCursorAccountUsage( value !== undefined && (typeof value !== "number" || !Number.isFinite(value) || value < 0) ) { - throw new Error("Invalid account usage totals"); + return yield* invalid("Invalid account usage totals"); } } const timestampMs = @@ -252,7 +328,7 @@ export async function readCursorAccountUsage( typeof event.model !== "string" || !event.model ) - throw new Error("Invalid account usage event"); + return yield* invalid("Invalid account usage event"); if (timestampMs < sinceMs || timestampMs > endDate) continue; const totals = { uncachedInputTokens: tokens(usage.inputTokens), @@ -265,8 +341,8 @@ export async function readCursorAccountUsage( typeof usage.totalCents === "number" ? usage.totalCents / 100 : null; const sessionId = typeof event.conversationId === "string" ? event.conversationId : ""; // No event ID is provided. Preserve identical billed rows with an occurrence index. - const key = accountHash( - JSON.stringify([timestampMs, event.model, sessionId, totals, reportedCostUsd]), + const key = yield* accountHash( + billedEventIdentity(timestampMs, event.model, sessionId, totals, reportedCostUsd), ); const occurrence = occurrences.get(key) ?? 0; occurrences.set(key, occurrence + 1); @@ -282,19 +358,29 @@ export async function readCursorAccountUsage( dedupeKey: `cursor-account:${accountKey}:${key}:${occurrence}`, }); } - await NodeTimersPromises.setImmediate(); + yield* Effect.yieldNow; } - if (removalsRemaining !== 0) throw new Error("Inconsistent account usage boundaries"); - return { accountKey, records, missing: false, error: null }; - } catch { + if (removalsRemaining !== 0) return yield* invalid("Inconsistent account usage boundaries"); return { accountKey, - records: [], + records, missing: false, - error: "Cursor account usage could not be read.", - }; - } finally { - // An early exit leaves the pages requested ahead of it in flight. - cancel.abort(); - } -} + error: null, + } satisfies CursorAccountUsageReadResult; + }); + return yield* read.pipe( + Effect.scoped, + Effect.timeoutOrElse({ + duration: Duration.seconds(60), + orElse: () => invalid("Account usage read timed out"), + }), + Effect.catch(() => + Effect.succeed({ + accountKey, + records: [], + missing: false, + error: "Cursor account usage could not be read.", + } satisfies CursorAccountUsageReadResult), + ), + ); +}); diff --git a/packages/provider-opencode/src/server/usage.test.ts b/packages/provider-opencode/src/server/usage.test.ts index 8389f85a5f3f..5a517e40c5cb 100644 --- a/packages/provider-opencode/src/server/usage.test.ts +++ b/packages/provider-opencode/src/server/usage.test.ts @@ -1,31 +1,31 @@ -// @effect-diagnostics nodeBuiltinImport:off - the reader under test opens real -// SQLite databases and walks a real legacy JSON store. -import * as NodeFSP from "node:fs/promises"; -import * as NodeOS from "node:os"; -import * as NodePath from "node:path"; -import * as NodeSqlite from "node:sqlite"; - -import { afterEach, assert, beforeEach, describe, it } from "@effect/vitest"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, describe, it } from "@effect/vitest"; +import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; +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 Path from "effect/Path"; +import * as SqlClient from "effect/sql/SqlClient"; import { readOpenCodeUsage } from "./usage.ts"; -let dir: string; - -beforeEach(async () => { - dir = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "usage-reader-test-")); -}); - -afterEach(async () => { - await NodeFSP.rm(dir, { recursive: true, force: true }); -}); +/** A writable connection that stays open until the test's scope closes. */ +const openDatabase = (filename: string) => + Layer.build(NodeSqliteClient.layer({ filename })).pipe( + Effect.map(Context.get(SqlClient.SqlClient)), + ); describe("readOpenCodeUsage", () => { - it("counts migrated OpenCode messages once and sees subsequent WAL writes", async () => { - const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, "opencode.db")); - try { - db.exec( - "PRAGMA journal_mode = WAL; PRAGMA wal_autocheckpoint = 0; CREATE TABLE message (id TEXT, session_id TEXT, data TEXT)", - ); + it.effect("counts migrated OpenCode messages once and sees subsequent WAL writes", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "usage-reader-test-" }); + const db = yield* openDatabase(path.join(dir, "opencode.db")); + yield* db.unsafe("PRAGMA journal_mode = WAL"); + yield* db.unsafe("PRAGMA wal_autocheckpoint = 0"); + yield* db.unsafe("CREATE TABLE message (id TEXT, session_id TEXT, data TEXT)"); const message = { id: "msg-1", sessionID: "session-1", @@ -35,12 +35,13 @@ describe("readOpenCodeUsage", () => { cost: 0.25, tokens: { input: 100, output: 20, reasoning: 5, cache: { read: 30, write: 10 } }, }; - const insert = db.prepare("INSERT INTO message VALUES (?, ?, ?)"); - insert.run(message.id, message.sessionID, JSON.stringify(message)); - const legacy = NodePath.join(dir, "storage", "message", message.sessionID); - await NodeFSP.mkdir(legacy, { recursive: true }); - await NodeFSP.writeFile(NodePath.join(legacy, "msg-1.json"), JSON.stringify(message)); - const first = await readOpenCodeUsage(dir, 0); + const insert = (id: string, sessionId: string, data: string) => + db.unsafe("INSERT INTO message VALUES (?, ?, ?)", [id, sessionId, data]); + yield* insert(message.id, message.sessionID, JSON.stringify(message)); + const legacy = path.join(dir, "storage", "message", message.sessionID); + yield* fileSystem.makeDirectory(legacy, { recursive: true }); + yield* fileSystem.writeFileString(path.join(legacy, "msg-1.json"), JSON.stringify(message)); + const first = yield* readOpenCodeUsage(dir, 0); assert.isFalse(first.error); const records = first.files.flatMap((file) => file.records); assert.strictEqual(records.length, 1); @@ -52,20 +53,18 @@ describe("readOpenCodeUsage", () => { reasoningTokens: 5, }); assert.strictEqual(records[0]?.reportedCostUsd, 0.25); - insert.run( + yield* insert( "msg-2", message.sessionID, JSON.stringify({ ...message, id: "msg-2", time: { created: 1780000001000 } }), ); - const next = await readOpenCodeUsage(dir, 1780000001000); + const next = yield* readOpenCodeUsage(dir, 1780000001000); assert.isFalse(next.error); assert.deepStrictEqual( next.files.flatMap((file) => file.records).map((record) => record.dedupeKey), ["opencode:msg-2"], ); - assert.isAbove((await NodeFSP.stat(NodePath.join(dir, "opencode.db-wal"))).size, 0); - } finally { - db.close(); - } - }); + assert.isAbove(Number((yield* fileSystem.stat(path.join(dir, "opencode.db-wal"))).size), 0); + }).pipe(Effect.provide(NodeServices.layer)), + ); }); diff --git a/packages/provider-opencode/src/server/usage.ts b/packages/provider-opencode/src/server/usage.ts index 9cc5ce2d0910..f0005ad61608 100644 --- a/packages/provider-opencode/src/server/usage.ts +++ b/packages/provider-opencode/src/server/usage.ts @@ -1,16 +1,10 @@ -// node:sqlite reads live OpenCode databases; Node fs walks legacy JSON history. -// @effect-diagnostics nodeBuiltinImport:off /** * Usage history for OpenCode, read from its SQLite databases and legacy JSON * message store under each data directory. * * @module provider-opencode/server/usage */ -import * as NodeFSP from "node:fs/promises"; import * as NodeOS from "node:os"; -import * as NodePath from "node:path"; -import * as NodeSqlite from "node:sqlite"; -import * as NodeTimersPromises from "node:timers/promises"; import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; import { @@ -19,9 +13,13 @@ import { type UsageRecord, } from "@t3tools/provider-core/server/usage"; import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; +import type * as PlatformError from "effect/PlatformError"; +import * as SqlClient from "effect/sql/SqlClient"; import type { OpenCodeSettings } from "../settings.ts"; @@ -93,11 +91,23 @@ export interface OpenCodeUsageReadResult { readonly error: boolean; } +const isNotFound = (cause: PlatformError.PlatformError) => cause.reason._tag === "NotFound"; + +/** A regular file or directory, never a symlink to one. */ +const entryType = Effect.fn("entryType")(function* (path: string) { + const fileSystem = yield* FileSystem.FileSystem; + const link = yield* Effect.exit(fileSystem.readLink(path)); + if (Exit.isSuccess(link)) return "SymbolicLink" as const; + return (yield* fileSystem.stat(path)).type; +}); + /** Reads current SQLite and pre-migration JSON stores without modifying either. */ -export async function readOpenCodeUsage( +export const readOpenCodeUsage = Effect.fn("readOpenCodeUsage")(function* ( root: string, sinceMs: number, -): Promise { +): Effect.fn.Return { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; const files: { path: string; records: UsageRecord[] }[] = []; const seen = new Set(); let found = false; @@ -111,49 +121,69 @@ export async function readOpenCodeUsage( records.push(record); }; - let databases: string[] = []; - try { - databases = (await NodeFSP.readdir(root, { withFileTypes: true })) - .filter((entry) => entry.isFile() && /^opencode(?:-[a-zA-Z0-9_-]+)?\.db$/.test(entry.name)) - .map((entry) => entry.name) - .sort((a, b) => (a === "opencode.db" ? -1 : b === "opencode.db" ? 1 : a.localeCompare(b))); - } catch (cause) { - if (object(cause).code !== "ENOENT") error = true; - } + const databases = yield* fileSystem.readDirectory(root).pipe( + Effect.flatMap((names) => + Effect.filter( + names.filter((name) => /^opencode(?:-[a-zA-Z0-9_-]+)?\.db$/.test(name)), + // One database removed between listing and stat must not hide the rest. + (name) => + entryType(path.join(root, name)).pipe( + Effect.map((type) => type === "File"), + Effect.catchTags({ + PlatformError: (cause) => + isNotFound(cause) ? Effect.succeed(false) : Effect.fail(cause), + }), + ), + ), + ), + Effect.map((names) => + names.sort((a, b) => + a === "opencode.db" ? -1 : b === "opencode.db" ? 1 : a.localeCompare(b), + ), + ), + Effect.catchTags({ + PlatformError: (cause) => { + if (!isNotFound(cause)) error = true; + return Effect.succeed([]); + }, + }), + ); for (const name of databases) { found = true; - const file = { path: NodePath.join(root, name), records: [] as UsageRecord[] }; + const file = { path: path.join(root, name), records: [] as UsageRecord[] }; files.push(file); - let database: NodeSqlite.DatabaseSync | undefined; - try { - database = new NodeSqlite.DatabaseSync(NodePath.join(root, name), { readOnly: true }); + yield* Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; // A busy live provider should fail this source promptly rather than // stalling the server while SQLite waits for its writer. - database.exec("PRAGMA busy_timeout = 100"); + yield* sql.unsafe("PRAGMA busy_timeout = 100"); const tables = new Set( - database - .prepare("SELECT name FROM sqlite_master WHERE type = 'table'") - .all() - .map((row) => row.name), + (yield* sql.unsafe<{ readonly name: unknown }>( + "SELECT name FROM sqlite_master WHERE type = 'table'", + )).map((row) => row.name), ); if (!tables.has("message") && !tables.has("session_message")) error = true; for (const table of ["message", "session_message"] as const) { if (!tables.has(table)) continue; const columns = new Set( - database - .prepare(`PRAGMA table_info(${table})`) - .all() - .map((row) => row.name), + (yield* sql.unsafe<{ readonly name: unknown }>(`PRAGMA table_info(${table})`)).map( + (row) => row.name, + ), ); const timestamp = columns.has("time_created") ? "time_created" : "NULL"; const predicates = table === "session_message" ? ["type = 'assistant'"] : []; if (timestamp !== "NULL") predicates.push("time_created >= ?"); const where = predicates.length > 0 ? ` WHERE ${predicates.join(" AND ")}` : ""; - const statement = database.prepare( + const rows = yield* sql.unsafe<{ + readonly id: unknown; + readonly session_id: unknown; + readonly data: unknown; + readonly created: unknown; + }>( `SELECT id, session_id, data, ${timestamp} AS created FROM ${table}${where}`, + timestamp === "NULL" ? [] : [sinceMs], ); - let count = 0; - for (const row of statement.iterate(...(timestamp === "NULL" ? [] : [sinceMs]))) { + for (const [index, row] of rows.entries()) { append( file.records, parseOpenCodeMessage(text(row.data), { @@ -162,48 +192,64 @@ export async function readOpenCodeUsage( ...(typeof row.created === "number" ? { timestampMs: row.created } : {}), }), ); - if (++count % 256 === 0) await NodeTimersPromises.setImmediate(); + if (index % 256 === 255) yield* Effect.yieldNow; } } - } catch { - error = true; - } finally { - database?.close(); - } + }).pipe( + Effect.provide(NodeSqliteClient.layer({ filename: file.path, readonly: true })), + Effect.catchTags({ + SqlError: () => { + error = true; + return Effect.void; + }, + }), + ); } // Do not follow symlinks, including cycles. Database records win over their // old JSON copies when OpenCode has migrated a store in place. - const directories = [NodePath.join(root, "storage", "message")]; + const directories = [path.join(root, "storage", "message")]; while (directories.length > 0) { const directory = directories.pop()!; - try { - for (const entry of await NodeFSP.readdir(directory, { withFileTypes: true })) { - const path = NodePath.join(directory, entry.name); - if (entry.isDirectory()) { - directories.push(path); - } else if (entry.isFile() && entry.name.endsWith(".json")) { + yield* Effect.gen(function* () { + for (const name of yield* fileSystem.readDirectory(directory)) { + const entry = path.join(directory, name); + const type = yield* entryType(entry).pipe( + Effect.catchTags({ + PlatformError: (cause) => + isNotFound(cause) ? Effect.succeed(null) : Effect.fail(cause), + }), + ); + if (type === "Directory") { + directories.push(entry); + } else if (type === "File" && name.endsWith(".json")) { found = true; - const id = entry.name.slice(0, -5); + const id = name.slice(0, -5); if (seen.has(`opencode:${id}`)) continue; - const file = { path, records: [] as UsageRecord[] }; + const file = { path: entry, records: [] as UsageRecord[] }; files.push(file); - try { - append( - file.records, - parseOpenCodeMessage(await NodeFSP.readFile(path, "utf8"), { id }), - ); - } catch (cause) { - if (object(cause).code !== "ENOENT") error = true; - } + yield* fileSystem.readFileString(entry).pipe( + Effect.map((source) => append(file.records, parseOpenCodeMessage(source, { id }))), + Effect.catchTags({ + PlatformError: (cause) => { + if (!isNotFound(cause)) error = true; + return Effect.void; + }, + }), + ); } } - } catch (cause) { - if (object(cause).code !== "ENOENT") error = true; - } + }).pipe( + Effect.catchTags({ + PlatformError: (cause) => { + if (!isNotFound(cause)) error = true; + return Effect.void; + }, + }), + ); } return { files, missing: !found && !error, error }; -} +}); /** * The data directories to read: `OPENCODE_DATA_DIR` (comma-separated) or the @@ -244,7 +290,7 @@ export const openCodeUsageReader: ProviderUsageReader - Effect.promise(() => readOpenCodeUsage(dir, windowStartMs)).pipe( + readOpenCodeUsage(dir, windowStartMs).pipe( Effect.map((result) => ({ dir, files: result.missing && !result.error ? null : result.files, From 6586a01f41bd692f02d565d38f97b52a8067317e Mon Sep 17 00:00:00 2001 From: Daniel Alvim Date: Fri, 9 Oct 2026 17:58:53 -0500 Subject: [PATCH 21/39] fix(web): composer context strip pads both edges evenly (#17562) --- apps/web/src/components/ChatView.tsx | 2 +- apps/web/src/components/chat/ComposerSurface.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index dbee50a28ac7..1fe19f6bc2f2 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -11773,7 +11773,7 @@ export default function ChatView(props: ChatViewProps) { aria-hidden={showComposerModelStrip ? undefined : true} inert={showComposerModelStrip ? undefined : true} className={cn( - "ps-2 group-data-model-strip-transition/composer-surface:before:backdrop-blur-(--glass-blur) group-data-model-strip-transition/composer-surface:before:bg-(--chat-composer-glass-surface)/(--glass-opacity)", + "group-data-model-strip-transition/composer-surface:before:backdrop-blur-(--glass-blur) group-data-model-strip-transition/composer-surface:before:bg-(--chat-composer-glass-surface)/(--glass-opacity)", !showComposerModelStrip && "pointer-events-none invisible absolute inset-x-0 top-full", )} diff --git a/apps/web/src/components/chat/ComposerSurface.tsx b/apps/web/src/components/chat/ComposerSurface.tsx index a468c862cd19..cbd769d1cf2a 100644 --- a/apps/web/src/components/chat/ComposerSurface.tsx +++ b/apps/web/src/components/chat/ComposerSurface.tsx @@ -86,7 +86,7 @@ function ContextStrip({ className, ...props }: ComponentProps<"div">) {
Date: Sat, 10 Oct 2026 00:59:06 +0200 Subject: [PATCH 22/39] test(usage): v4 cache upgrade test waits for the migrated cache write (#17553) --- apps/server/src/usage/UsageService.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/server/src/usage/UsageService.test.ts b/apps/server/src/usage/UsageService.test.ts index 68d2e7c2bbf6..9c1e62fe1a7d 100644 --- a/apps/server/src/usage/UsageService.test.ts +++ b/apps/server/src/usage/UsageService.test.ts @@ -1175,7 +1175,8 @@ describe("UsageService", () => { return text; }); - const summary = yield* (yield* UsageService.make).readSummary(WINDOW); + const restarted = yield* UsageService.make; + const summary = yield* restarted.readSummary(WINDOW); // The live rollout re-parses at the ultrafast rate (10 x 6); the // deleted one keeps its saved v4 usage at the standard rate (20 x 1). assert.strictEqual(totalOutputTokens(summary), 30); @@ -1188,6 +1189,9 @@ describe("UsageService", () => { yield* Effect.promise(() => NodeFSP.readFile(legacyPath, "utf8")), legacy, ); + // The migrated cache is written in the background; let it land before + // the layer removes the state directory under it. + yield* restarted.awaitPersisted; }).pipe( Effect.provide( layerService({ From c85dc642681a944e644cbbbea7257d274822c21a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 16:02:19 -0700 Subject: [PATCH 23/39] feat(mobile): support Duo in the shared iOS app (#12648) --- apps/mobile/app.config.ts | 5 + .../deps/react-native-screens-5.0.0-t3.7.tgz | Bin 768221 -> 0 bytes .../deps/react-native-screens-5.0.0-t3.8.tgz | Bin 0 -> 768223 bytes apps/mobile/eas.json | 18 ++- .../expo-module.config.json | 3 +- .../ios/T3KeyboardCommandsModule.swift | 6 + .../ios/T3LayoutMetricsView.swift | 51 ++++++- .../ios/T3NativeControlsModule.swift | 4 +- .../T3WorkspaceOrientationSubscriber.swift | 22 +++ apps/mobile/package.json | 2 +- apps/mobile/src/App.tsx | 13 +- apps/mobile/src/Stack.tsx | 10 +- apps/mobile/src/components/ScreenHeader.tsx | 16 +- apps/mobile/src/features/home/HomeHeader.tsx | 45 ++++-- apps/mobile/src/features/home/HomeScreen.tsx | 16 +- .../layout/AdaptiveWorkspaceLayout.tsx | 29 +++- .../createWorkspaceStackNavigator.ios.tsx | 68 +++++++-- .../layout/native-glass-header-items.ts | 1 + .../SettingsOpenSourceLicensesRouteScreen.tsx | 4 +- .../SettingsEnvironmentFilterHeader.tsx | 2 + .../threads/NewTaskContextPickerScreens.tsx | 4 +- .../features/threads/ThreadDetailScreen.tsx | 33 ++++- .../src/features/threads/ThreadFeed.tsx | 48 +++++- .../features/threads/ThreadSettingsSheet.tsx | 12 +- .../features/threads/git/GitOverviewSheet.tsx | 5 +- .../threads/sidebar-native-header-items.ts | 7 +- .../threads/useThreadHeaderOptions.tsx | 99 +++++++++++-- apps/mobile/src/lib/layout.ts | 47 ++++-- apps/mobile/src/lib/reserved-regions.test.ts | 140 ++++++++++++++++++ apps/mobile/src/lib/reserved-regions.ts | 41 ++++- .../src/native/NativeColumnContent.ios.tsx | 6 +- .../mobile/src/native/NativeHeaderToolbar.tsx | 32 +++- apps/mobile/src/native/NativeSheetContent.tsx | 19 +++ .../src/native/NativeWorkspaceColumns.ios.tsx | 28 +++- .../src/native/NativeWorkspaceColumns.tsx | 8 + apps/mobile/src/native/StackHeader.tsx | 2 + apps/mobile/src/native/V5StackHeader.ios.tsx | 24 ++- .../src/native/createV5StackNavigator.ios.tsx | 10 +- .../native/use-native-mail-search-toolbar.ts | 13 ++ docs/operations/mobile-screens-fork.md | 9 +- ...act-navigation%2Fnative-stack@7.17.6.patch | 12 +- pnpm-lock.yaml | 20 +-- 42 files changed, 808 insertions(+), 126 deletions(-) delete mode 100644 apps/mobile/deps/react-native-screens-5.0.0-t3.7.tgz create mode 100644 apps/mobile/deps/react-native-screens-5.0.0-t3.8.tgz create mode 100644 apps/mobile/modules/t3-native-controls/ios/T3WorkspaceOrientationSubscriber.swift create mode 100644 apps/mobile/src/lib/reserved-regions.test.ts create mode 100644 apps/mobile/src/native/NativeSheetContent.tsx create mode 100644 apps/mobile/src/native/use-native-mail-search-toolbar.ts diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index f61798d53d63..57717d2d8458 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -264,6 +264,11 @@ const config: ExpoConfig = { "keychain-access-groups": [`$(AppIdentifierPrefix)${variant.iosBundleIdentifier}`], }, infoPlist: { + UISupportedInterfaceOrientations: [ + "UIInterfaceOrientationPortrait", + "UIInterfaceOrientationLandscapeLeft", + "UIInterfaceOrientationLandscapeRight", + ], NSAppTransportSecurity: { NSAllowsArbitraryLoads: true, }, diff --git a/apps/mobile/deps/react-native-screens-5.0.0-t3.7.tgz b/apps/mobile/deps/react-native-screens-5.0.0-t3.7.tgz deleted file mode 100644 index 860521672c8a4cca87f2981a6415fc91a5a8e779..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 768221 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∓WXtOh>m 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

OD#}a(L_6P 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�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

_ya~*enM}UA30RjFhb0o$W#;nv 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 z zchV>g%=2@isdaL7H%o!y2>d)B(`;z{4TtVT-S)Fh||2Nkwa# zV=%-~uckvSG#HzZ*KsizHUJx6?>+D9v;>q6$z)~1cntuFo85PuD9t)m`jL`Ah?*y8 zZ(I1Y!9Ac2rk$1n$r4;10*C3zs~J@};hgxY(TvSgtmX0mYs!@8!}Ra<(Z_C1oA zD$V^rVIz)4mxjI=qegNB*+YOw8lM7Z0zxqX;|8vIw90m6txUO%Ur|TKw?A+Atoh4Nlg6h##)qPaY-lBkQYh%0R~Y<@e;YFzr}&TGc!M z*Ow0;J~n&J4&JYE{;TQr8+AhV{h6@ON`+kv|A3UiU8}=Akw5zyo|y!~d9byU;+D@R zs!zx12s|Q`HWp#{s}Fi>EtyBq(iI~^Wk4`I(|8DimaCVd;&MV+nF5fmJ{KPnJl#16 zq;7N28!8E~-KNROY{V2f!y$1O>S&ebcOkkLsxK0?b};nj^%UWDtQ+j? z()sHs2Um_#2~tm$S*Lmsq^EQldh_{sb`Bv~*;IsN>;e-z-&_TeO&fj48`TU`M(5%* zGavvpvnA^@cpS8B7Odzl@S@$7M6|s1DOn7}@dayrJ>FxUMl^SOL|7YTA}oAC;0eUI z=Id4dB_pEk<5ilC6v~(|t#oC5j^Q9fi$$JKviMVUAIfs7$4Kfbx&MzEt<)3*JQo6N z!*$^MVKU)FiP_lX?kYv$@S5!s-zW@B%i(lw8bqXeUM1EbXj(AcVBB7a)1u&r7(Uvp(%wbK-{SOucL#euldVYuobGzcP) zT3#5X4ME_I^({AQpf2F|Iw}+bWeEz`nGo>pZ}Q3vHiB}63oB<6hJD>aH?S6X7ym%R zxcc{Yx`9RCR5yt5LRUy%?xw(y7LOC8Py=6YV`Bp9;J;J`c3*o3*#I6LyEDh=5pK|d zu(wqr+XHuERK}Y^x*dcqM6p{;)PAt|GK4tvn09KQhhM}q&DT7i00h7Y_3+3LsFb`F zHCR#^e0O^FGTebFof1|V2nR4Z4}M-m>%8HOgI2K@N`qC8ygs?9-BPO*-%?cE&1@^W za#|}0ds6|U3YXA9GE9*E4;i|&rN}YbMQtZrgfu*!s6punCYfKe3%LGvIZ)LkJt2J@ zli399Qb96(q*w>T=9K)HfuicS8pDllgr^B_h2dTXyW#2OO>700pmh;y(for22b7hfx)Wyo#_z624R;~s z1j!9Z#TZMEkx(9*7qyS5AC=uW>ga0YJUXgZ1&=hbh2RMktB$DljQtGx1|geyYUyz~ z6Yx2&0?b~Tj&fqmn8qfpb~ZtBKuAn%-V-%V-7vX00x}#3Czz&chZL7UmWy~!T}yej zHk8C9I`an6j&mOS!yn3x058$v(zm%{FB}h$9f$=^g>21$f!NAc@V}n9f(stg zU3RSCnAh>^oe^|WSEc#DjrYsW>$lGjci#bobZhhF%fqM7f7#w?(dF(e{g0TlOwEQ9 zOcfaCm28(pj(eUBlM&b~9dE=CGh$;39yJ2d`K%DOBedTT5vB(4q~lx-*IQwEEdye5 z`XP!71Z#pp7?6ag$+W{?ndAM=zJ9og{n2QwTYE3F)92G^Hf{fWS6J#a5j`NNbG3>H zV}yVJFp1NXcRTBW{F&Si57jI~AxkJ$7zs@_>vfs`b-_=>SatNgkk^!^}Vz?Uq0rty8vgQM0+F~W>7jO>fop`1s{~t2lN&Hz2|Gm5z_VtwwrF=4_s~f$> z75Ld})XPuWhuBa&ie(*2rrcZ|x(eQ`ZrMG;J{SQEMwnAfQMr|kC{wcA;W;lDi94s< z4Nza+ZQ}qa>|5FSL}}6Eg6M_`nfH^L zWakG}M3D`$5$3lqy+%PwQd3SnJvsEwS5=u?Sm0Hl?5Ma^err zEOA7QipMEK5zvFBA$F`6wTTnAAN||UU8;}%qyfY~G1D(5FgA~N?&>OGsdOuwYZ}(Z z@;df&U;+g9d=Z}>bV${r`dh2TiH*odC;Wtdi zbOTI2C4(6zyyxK2qa`05fi1$9)sUlBKpMcabodJ6;t=k#JDbA6vxnIji|J+m;7Dr( zN*zG@onRm7n-|%1FFx(JS=g(;J8HyncT}F^^@-{5k)A39R;2o;>>7s$)F$9Q)Fj? zC}x%cjB{5K{3Mw!APT~}KGa%&-=n-M^|Kw)CZi`*A!dZ|H-CoPe$&~}FcdYCXYRm| zdC(39mfA=#s+P?#0gL$tHb7k5#E1z=SsU&fD(gVL-Dn84yluGlnpsRjEi=kvupUkt z?OwTq+ls3~g-^Gud>0Og(cjY1h~#LM{{E``lG5S}`>BM}2@2$@;{69z*}3qCv{ohHRAZDnjbt~~uh4>b=psJaDb@sNTm^^5&JU~>!ME%hYc;f_94iVQq5 zm}d%S#0FfHju%tE()jRMTQQszPJl$I*m88le(4#oDm_i!jaja3kKauwwU(H=gHY&1 z{Sq-J?TxLF-{At`l#pbp!z4Z+yC^H&abhyDK`y2^2QDCtU@c7022MlILUi$X$%|3? z0W)1_hbhHRLll|11*r9N4qsoG#Pv{S25@to4-Jf?*%a6UG2%JCK+NSyue_*9Jnxal zVLBk(&I_&oJ@rVxzm2-3!vef`A+$t0j^Fy$4Zb=P)~y?TxuRn$c1Ok@VR(vR%Y^{)3Eusxn8r|QOp1Pve=zt6o-$(jdWR#6( ziBdt*Ns&#}P2;0dou{2a^=}fO$0eyIse=&~XXRTz1r2Y37oM~`A*1KB%W-_34k)Xg z5}_Y33Eo7>QjF}O^=1-5V@PkB_nU`BVZ`=>$M+wv!O9;dC!o$BKKu8b%~#u7hcDi4 zzIy)K>$iX2h4G_&j9EN%vM{R89;ZS@39X;i6Rj4SVQTJr-bjsds#gww`DWciI}Q%u z&u-oe@8MVRI97K>Zp^=Y`|qFiKaSvAPP2R&;?o9~%i(a9-ud}e>sVP?d9=0`g}zt6 zJYHK{iT>rm!$&KttB)S7sOMHzAFZxER{ws90hobXR5j(gQ(kJ z`#N2EOa@^{9F1X(nBfexK(Gly8M7SGj8uKDa{wXRC#sH^c~#%AA<5V&fGH=*pw}Xg z5&4#qPm^i1v%8m`lmAl{0Me7YWj&eCbOSe{mEOa3>^qqL)p%hyq1#}0L9@%`4DT80 zWIBfsa8flsiSR4N*#K}8Ej0s?%Z#iyISqj(0tpON7s}9w;6?J?$He4)#oTEA#QZ^~ zar%%X6C@qs_TvyMy;z-+z3AOIEzm|x)7E6X$^-QR^#Lv)N9_R&Td6E!`+yY(B*U*K z(&w(ratc~quPNk3+^CF|-u-B~LA%%IwRa10<;}0`;k9dnjy0=n^#&_@2ASLE*VwQ^ zSY$}g5+UUnZJBf2?~IUA>+g(zffWzF8n~^g#fCd(7vP?>#nYS$QHz!Wo7G}Q?Oc5g z1#)IIK#fkb7*Zq~xs;m~4-h}K)JIA~%sEALUYJ)Mk%E;SUq+h~wUz5ug}qD1pEgHI z63`H>&r{M&Bl?hgZyYT<&{0o%GH0{za+Wx&1FUrJR4cB9U>F zFPH>iN2yX?gyu?brBNM$U2C7&uYO1ndI8dVMOV+6I{2~e@h?R#G6YY*$p;gz(DR~Zm{xRP#q0o?TbUw8T& z%zi7d2JFs0&CsZoO)ui<5J+=)@P{??1dL@w&S@AtneWqPeQ15DU4GMh{Dx=vniD+t z5b!;$X1XZf5QT=w9-wbgM~PJ3F3VR*tB1R`g1n?;*6lzh;cGS><(asS(z?KOg=oC5 zG=)5Sq4LTzN^ldrKMb^c9rN?(+)2)nZ60}FnIw+cijxspeRNq~;hcY|1zfM2CrVeP zZ3@A`l%CUm+$cZtD*iwpi2b7N7T#4CqxE3Pkp6U`f|uNWP{}3{qNkCESnubud>fA1 zrd;3V%DM0;Ero|ET6|bSqrz&lCfm)8-A~^joc(8Xbn_Ud1Le29?4Njx2}mPx2hwH% zi$W`hcI9h|R4@;dtXnX_~ zDVX7b_3B)mThr2I&NPbE!rcjk+wFO8ZGy4=)4>7L=Ti!PasH!C{SCX?? z*9}wSyw^&5dV8p(jLDB#y@rC*8#L^Dx?ix@TNY*X7e1hPAk?^J?9oHhh5qCwb`Q63 zU##0I@UYBP*JPaM4%iKhd2oA@sw9kNcLI;uZ+?qyo}~k=pB=1M+VNxK%Wx+L1E?h_ z=Z)B4<_}U}JufV_g0;93tYXT)lmM--e$hR~LKXDisQnkCd_Pt4@k58yVpYIj{&sUK zwaJbNopB4w6?1GDDYEW#)q)hcuN5K$yZ=qT+?sq5yl{6K8}{bw0Q1iNmQ1sGBU-?% zob%Nuy}MOyQsjyw(p&BaHjnB&xjsj-SD0obCC*tAJ&*Hbd;EF^p^hc{oa7WjSOBM7 zaCrOEvGFwvi_SzCSr$+XecW?T7ZGOgU0Wo484F-!6&YE{WeRiQg`XH+)I_z26Zb zmg4JN4R7*BxcEiz3jjE;bwRiQK0v|0Id5dRrzI0F5?eGE>tA8tHrc^O{9U7pIB*fT z+JE&!ziO@PSNZS8ZwQTB`_He!e|eYJx9xZH+pkKc54g8}7aMS1d;fv9V4XGkDdvs; z%J%46vPfS=6)78Ui)=!JB#f0N7yBHOzu9uXvMu)?>2SR5aLu~SJCa?7C-LpC!X{*Ox4#G% z_Mw=83Z1jgdI_$fpkB7`jQ6*zXSJ|SKBdJM7uLuPaR^m^Enz*E`>=I|E6xq)iAIA*A#z+Cl=N>388-{ql#$4R4CzW$tPkOpOQdOCKMbkgWlUOq-48T+Y)<2b|AA zbvrl3>)#3&zXASEkT!-d!H)N}3?kvR{n`9AkWv&#RXEqJ<#V`QsbXdPKOE%ZzxsRy z!~+m$enSF&Ljr!GZL!eJZt*>|(6#ZKfc%;QazQn1@wL!MQ~&bv*CSVZGdmuogJ|>3 zwwqiQg)c=fpc<^x7%6FMLmF}=imbM5#zki6xlWXXPS=FYZE3#c5_|UB6`ahLh)y=7 zZJkbcD(Re+>OBFT(k9O`yLvEL)33=0>hPaU*j7zuEFQ6{VydpKJ#h)ZaaLaD~a6T&_{7CQfQ%JPBQ~fF}-OH za-^nt2s5l+#FtcVdOFUg2~FEH`RgpnAz`}7fr5Dx6FY<5D6vH5 zz&{(OSVuOSMn~o;t%V+8_NX4!^qZGhD)jitY>d%$@rYllRJ*uW&X;UOtr%_rk|E6+ z+$MuTHXSlz=dLQ}R6UNPmA>AMR@d%AHlJ5Iwl&?u`a2zU*Lh+S-8l8+XW487ODOsu8_`<# z;Uk`bZO9pR0w4=YX$$1kxlYff8mInGEjGxOGlXofs+DKSG~r(BY@0*%$Ovv+y% zq1S1c4Q7VBczKq7NDKA9G=?;}6Ek_stB=-x{9)z(63pHoNAdV{7M~^?+3|l2MCy%Z zlaKc&>iS!#yT8_Z_=t4t{}QP$=jMO>6&b{p&^$>`ub2a}F8|}|+WklOS4;UHAFn+6 zmjCf9=6`f%UF3lL=`G~iSjM!pV|A+Jn40kHN#KDO@$nQ0g*c&N{$?~gO)DQ(hjRFr z?Q^v}ogBC|&%Ml`*)oXp(|Jz&RkyR znITUnGhDQXTn%}|#k_wg;MB5-Y}revU$@&*xbt4Y`&V)CiYhehRLP{?py4HN#masB znnb+PU&6KaG>MDh1$1M*Fg2m@4o8G!oL1LV<~4%@wS*HS;Y!~ic*yG6{XRW$s&_`a z2fMS$sVL75zh7^^+5GtKaB#SD_-=dfW^ea!aIo`#wgJ$~^=BK4eESZ|$J-DdKcNbW z=r4$9MK+U&IvhBFvk=rfC2$NQkh)1d^`V%LotF>~9 z90zIn3gnbedj(cb?03|s54t7&p{pG7^XFDQB_YxLZTrBfFL-n$+Vg*2T^|-)9dqj< z##jT`Iar#k>K_4>$Vc!yy1Ga!Ub@X}w3}Rwfl|x{$H&n)0mjhfII!%X!RjoU3c?Q((l6)XarYX{JP(VR|Sa4gOY)f zqzu=ii-by)#$socVn`Qol{m#BKk*1l!8{bXcB!Z-Tn4F>aHS`^7agKhem0KJQ8b6E z9jrYG?oomjP6@c^()cX8Oyg+I^5JEUO3~xZ0Ji)%8W2wV=6vs6X}C^|qWfaZ(EMalAIq5 z&PfhN;+{tYk6ts?sNbK&pOQgI1o$mColD8|{fmxB-Z+>Fa3*x|3a*e?u|hB{S%`H! z8{(Qhk9POO<91X-XUn)u#77IQhx4tqeKE;dt8L#A%?{H4PJa5lBEQ-mokO2MVx{r$ zLk>*xr_WvKjfBgFt3}gu9I6p3ts$xL?;?ds%Jgj7Tn$OWW{XiK49o+p@o|_| z5i=7XoZl;!ILSQrLC z8ti1*}eSwg$BTL zV14iK{LcZLws&9aS@$F@CzxWhg>sr3xEeK@kewpq1wL|-k8~yaG`q|{aaCxF{JI7{ zH;Kv=tGUw$)UBvppI3s*!|oO3y!Z@AlLCdcYb@b*1uPN^!~t8pCcWD@KTn6&M8i>k z2n-c@3K3>^ivLR`14+V1^AP<3ZBco{bwp8})oDCkCBshL88^#7ibg%;arH{E7Q{nn zf@YBDU;m00{ERZxTJ;gPd*?kkqJa71+~#L6o*@c;uwt`QQkRwaS#K7912KmXZG z;4kL(D$Pd7qAgLAZTNUj&JvxoUn7KiTetwxd~tC(T(XDzJeBhLgESFGA9t*4%aU1o z5GDb6I8GSQC(L!JB!F@S=8#uKD4dDRK!|~hC>6$5fI2vrF&Z!7yyfG-xPUno(YkV8 ziL~YRxDbq9e0QLU`bbWcqKg7IahdQva61DDqd;^0u{n8Sli?)dr7RioPZ?b%9@E1< zZ$$P9>H85e+!PeXyr1`z67{wKI4}gc?;T9^zNgK8Y`9q{^Os4C(gR;fbYB7;xO@X|JWGFM zla%VB$N_^12U&{?0&!ZNg%RFv$IQ~rnJXh01oqB+z*BB^)7o zgaBcb7QhbRkRXFwG|5Nt7+&Y%T<1VZ1rsW4Zpzu<{m@rDZ&^b~iH%Ufr zDQ(r9+R<6-k$H(%pLQ_3(I$s3~{Pu_&v8${k zkax5w#~x|IxTBH}3Cs#!eel0soT88uN1|@>sGogJOr4w$^cBCW+bo3~KVT*Hiu63q z^nJ^CD9LLF4~rD`SM<01Jcb>fjmB`Y>>mP~M*S*u3kS;msE29kRYmU#mIlwaxQu#~ znjP5zev?$gp4LNZgtZiE=qUv14Enh-Zg>)psIE93BlatJup&FPUlc%K{Lv{nJ=w4M z*=IvbiVLCfqF>KZe8<1=e$7kO!KLaNpE*)BPbYs#ulQ5?vtvv+r+**~Se9HZsPeI} zT=7ljii?rX+NZjM)5z92$^?{4!DCyDLGjq*j`nMxUm? zxCR{{E<6+d*DRSO{XRo_PipNMDl-A#U`N)QZ-m@$4|&rVMz?li@azbUKt<;OkM$51 zc1L=9?VVYMtiE>Tc1&Dw70ArrO80utdTd)}l(5KJTRbYD4e4b88&g4aWI>U{gCF&+ zHzJXVyc-&fRe3AzW#pFh=;_zU1;)TZ%3(ZBP<~3Z=oEv*bWXBIbb{Lwozk~|tk8Bk zVjg-z_u(whR_OraTaa+rp;_(0VfYn#F`k%$s4l zqTvwLCe(wuy}l~BgPeD9jPl@k+*|rXN{8WK=fm5*-R;5N!QlJt{ezvo-NEMfn>+6| zU%lJzJbF4<>&iv3l*%CQv1mfhOWfZpsrZyD7Fe4eqtd&MR_$}FjL(uypis>RdA0ul zQwS))Alf0kBo)EoQ_7|QVq(3PCzez$kZYJ`PcGS8Jsh_@A;Abh&P~UNh4^6 zDXMdYK=-u0`7Fhv-qe~nPeZ5rdiXjmry%U2AsPJqbRvfN@+S;?nOIv*MN&(j>*t2Q@A{nzhTAZKxASoC>&oP6GHK} zXH-)^m8wR;X#>OgOVl)Fx}os$AYgsgTOoUYLm8w1Y=ycRqq!(FXJaAmp?|R-8#|i| z*Fgt3J!eLyDiT=q{ziM5pE3+-j$}70@vB9Ns>Rn{YBFATf^!!dct2WGa)^=$S-D^! zpk4$pP@?3LOs6Q+hI%5Tyie{G7jY@L$le)R0s5UeW`Zu76MjzX23c~Iveb6lkjhX! zEHr_7hl470D@=@bU~RZ9Y$?Z958i{(>_{G;SDw#WZ2dV>m1LY!8B9-lXrV;&fT6Xy zCykzoL4Eh{i)p<_c5wPm7#A`Qe&d}ljJ--4u&1>v!{(&AVN0vN}@euAMo60i)UJdDK~058drqY)f8?5gd%+5QMF*e4H13?jA1R@I&$5vpkd0 z7r=8y$X-!tU??p~m$jO7-nKZH}Z&nr6h!po_W=+L43?iwmDV2U^a7=>Oestqyv$ydJDcu^m9c3_TF>Ey z?2YZ*E2KvyGYk@2K1nE)u8f2yVEW&&JCISHup7oSpOR9W5tmw9{9*Kanw;eYH<2qu zS%q+P@Cc39=thFpQfnNU0Z6X^>wpo6Flyl~JetJWj5LEda7IMfp-`G6qo9}zr;EHm zBLKFAi`zN;IifDhtIwDQ$RS`j_?^b|bXlU{59@#A@;!Nh2Z%DNr79r|ix7{=}+ zy|I`lBo+I)v5EyV#2OolC2Dbn|8ZrZ7$&!%P7CxIA$9xu$CfE zsBKYg>*kBB;gICDeKW6RK2%EX1MCEmE(Oam{ehg0~MBP%;Zg8d@RO&ld%Z*>fXowcfI(u%*Qjd zEfkt$?W7r~mvs4(r(p>b>NLbBc1s{D_?ndZ`cJakOsCuboTZnYMs`WI4|Z%Gke5kP z41ir!w(GJEntEXz5ag%aU45bZHMy$R<=(T{EY_!VGB@`m1p#BXMq&0CazN_-9y9?1(sisGH-sAWlsSQzN9RNKA1wG{306c3We{GpV&C584?#_I!{Q0NuDP<*O8mA()n#A zMY>}gP5F)tIVg(al|Wn=d2!j_N}QFZwMK}8@~Xi;2Q}~}Kz8{T5+gC-SC@gZ>S7P@~5};O|)j=Dec>OIKlK3KJt%a;d%D6 zds5xbj|w>>@H}|>SGb?&0@OQuM1W3EzC8E_gNd<$}DLB|1w4N%aN@eyk5hB$A0B%)pp zQGCF!`N#7}kG^NT*jTczdi+LerfQ$;Xl@{7e^w+%dejl=g+rZGsj7T(&87wSIp zTLA6>>EB-4Nu1zf@-9E$E{eS9yj(_x#0Jvi$k^msH&)K36e~UldT+VwL(EAa)i^j) zSp2x7I}=HWIAlvFgL;Qx3#4?)-+@gN1gbCq%W4lg;1duAJbmE zi6_h*$RgF^!j+iLE$P9zLJ5}Gf}`vI#Xo8U;5@Kt7;Q- zB$B5Hi3$JgXT}w>%|ATLd1Cth`MN=|Hc}ikIWL8=q2c$hw8Q#h9n&^mbPDh;5Ep5@ z*zA#x<2~@N6ob->{(5iCUo^0J2|w=83ZCZU5%Wy^xpziBVMj!iRJiQ>csxr~^$3^g zRCTK$iV4`UkNu1>IPoyZgY4H1BeGvAZnUJL2kl182p8s71Yq;mWm$_1B8oJ|?Kzs; z!(T!#`it^F0`SIRgx+QMb3zYi-)I;)RE@ESM$A=tu;&x% z8}X0lb>(BLK&cyqFpG$`3mIHVX4{MWift0)BEpXSTEmoLNGl=~OlbH521;2NV1$N1 zX`c~&kZpwQC-g#T4_uEpGp3Q1cH?W=E&7m@w@S<~osGv2A3c8jRELSv>Uy$%DgLp0 z@DWRhqqbgTK7zfJ{3yJ@Wq_PscT`is`@J(xS0EWjeP>LIw=yG+K+tlJMQ7(q zt}hS=>;z#>BCeldb3(oeQVFfk1lY|vvM~vTG2j79 zMvV{n2)>8DH3f|LW0@2?GC?0{wDfRiwRh|$RI`!Ebx_Rlm2lDStG$aO{Xko_&pWoJ zmDj?Nu?ls>;p;OX(RZqKsogF=FX^Sz;6!Kw+VGW5u+}GV5|{J5^Fi1gU&YrY@RwGt zcQOCVhH>9&ZP6g3mTOw^*|TWP<)G0$a}*cGHHhFFm?cd|A? zgDJ{MB5u9xYqXTKUZc@h&R}X8VKzr2rCvwVJ|}$VWEt0viKpO0hpg`oT;y5_CoIvQ z^wfnqU@Hsa8L*l;9x>!-%SqE5q$1R<3!T06i=obhszw?1b;hhTgTK&KOf5Y-K@Y40$nr+0}cfd+wa6j+P^a9)Po?{YF-KWE_;^^ll zJH^}xcnp*Wv#H1-<^$l5K{DJ$EXl)65@>g2MaPu&Hk5b4-^e?1yTAsLt(sIye`kpQ zlAbg4hde{pk1o6D^{^xaP4-j6r?j8tEzd}#r~G+Ez_?7CTFRu-BDs!E#3?=Q9@wZy z4<0Tp8aE=A!W%gvH}DXJ1zpqPQ!*vigNX(3*2$^r|=4C(Bd zP8imU=z+Y0Nty~%I%Ok^k$;rg*13uvf68MqTnRvOS^~1!pt#5#fitDS24?j;T zrz^9TU*oPaLF-S1J=%83YBSB0^zfGR?AzJ@Ruk=KO?N!gTbKc@=r8aEokZr^_&Bi` z=^Y@b(&w99nsI9?hon|K)bmSHSQ=+sx1BuMC*y~>$Xplcm&g=vk*UY z9uHtma8sC|M=?v2Cm`VSwVd4@PFRkdV8sJ`zwD|!mlabtJ)%a}Sv+B{`d;*3*?(n} zxb)^(rvCIN?JrVW7$Xoy_|bVH?|hDR3UkA^uE|N%`sRTicw?v^vGDrA$9c%C*nIzi z{9J$SB++s|CAmf+i2B_Fl8rd_UPHll*4vF%iB{G{q7`$dU&fa}Zn(8OR1@oIZ*3)d z&~@FWC5C*vFOreM@&g`>lSF1jRiq>zZ*)t?PnVvF-d;+;nzhXTbSNo5+w z`Y13~GZhaA7gZcRB|q`}RJss?0A=h+%&|E;KbO*$Pz35+_oDon+edz8r7mUz8Qz&D zN!g1Ia*!bK&lTRkCzQM~8J##iCn8-p&QX!0`xfoI6}PvvGCaJ{r*SsL@Y1hze$Xq* z34}YI47#Yee;`%*kMKB2IaLK?D1w6AZrF1ids?xg6geLnM?X{^aICyOqHle-b; z;HVJG8qID}`L{!milNsXfsF_v+uhSSP9tK0B#NS{RC+Vis9}sVMT^WCzd1`FNw{da zNl6<-&Z(Gt{5%#l`c{$Z43||+Cu#@QXu+8=Ax4WNg=%6kPvP}dd`+i}Xfw&Pv|=Yq zBZe9E5tzQy?I%lyNm-&}2O4{@6eVsdS!7DY*pWgZtqg6)OeBBBDIC>S1HmgyZ1br&(#iiCF&6<_DRu-s7&rpX@cu*?6`zljKM9smX#fR(J*0d z{YY}_@XfFe@W%3*zRa5fn+Kf8+2WHD*ev9n^sdRlbG$26NAo3E-d#abNzXH)BYlI# z7)%kMdE%37d%fO?`K3CoaldqhA@fd>%dda)Yz>t_C{$KucWIurWzH2%+8$1=7ZfB& zFG}g6sFTmzFqUrO!Lwytm-=D+j_2!S7Q}zQ4&-J&NZt{6P*h1_ekN-d1Mid?x(l|M zh#%PKo)E3V^=Gq$L#yhTrd;GSE0OJ@9#+cqF&3C2%uIVQ$HndL3HS6lRysJSeMUOL z3*-}#xVtIsyG~{lYAEx5b8T!k&eKbAh}-@fWS3$ulBIeq zduNfE>5|wWUYG+hT9XoccrokrnQT1QuTl`}W-M-Cr_rSEMYgW%mkNTwTH9=Zx4TjfHpv ztFc9;Uoc^mEBe)4b-19oIA-X3+cD=&Ui>w=?tk2=6?(@L$c6?Do#50a2RP|AV@4Qx zs;L-ydB&AIhy2QUXGYYgfq=+5Qci@V9dRwtZT9M`DF6fdCSO>${VPOqV#S)M&II-O zxaMxNFfCi@k?TGj{v>@OEt_=kSn`M%!r#OEzUE{pe91OH>DbwU5o0Say>G2!!u>s3@ml(xWd)4=X%TwBtIQFZuW~8L18^ z)u_`QyRq$PA-Es8(4`o*bEXa=Gs(ZK84+WumZ3=d-l2j~bf#bDv*m;diB%R-G_37_(dkCYrU10jrx4QMn==ZCsY{YJF@N_e5N8 zTsPra!3JF;Q2FH4Ei!x}g)Lw%J!Lg!k04C!&3SyT630uH6VAc+M&hSR1lXg{B7M;) z=Z|MJmIGJal5V_qeaeiD=4AEu)le3m_SPd{1V`SsjyDgRk81OM)3}ZnaTboLjRe~d z<2cQJjy}0G-Hl^A=KSN@E6RsolwaUm*T*tpM~PRSwGqoa;tZNvNAo_(W}=p6eMy0M z@x8dBHGwdr*gGymHsYbwU{|c_d(7`mjXfn>vpYGs z19$iJwQv3a>jHRS%R#aJ&C_olJ$m%)v7uDGchs;hX}SH+Sv>B)l+y;RTZJ7)k?%{Y z`l5ZH=98e2@$;rJ)beY8SaA(KGWTP(b1e&p=fIS8iVbB*!ZVR4)^h4S3YGHuB*RM7cZRA=5Y zQzMBX28WU`W2yh9@P{m6_ev>Y2;WL7jeD8^CrdHN_l(XPlw3K>AME$zPFeFnU z4@Ney9Kn|t5+sY&)PR$@70Fj3pJ0~H&Oywc#?sC7?)2z?`m@eF)zOh29ktB>oRVkiUDy3dR0_|6_BBh-sHuQT=EX{eLGY>S{MugILs-N zy_Qq(Mu+yDN1~1}`7TZqkjBdjrFOb9>TamACe@bfA?-$gSYP||`jWEv;Ye9+QwC5$ zEw+ui>u^aGualARG+=Cq03=H4%cb_cx}ECK9oZ)c>{AqfkW!_afF8HV*=(0%p?tAi zDGhroEqv_gg9Q+i!2`l!So@}<9}a#Gf_*aD$#yc4f?Hfqf$_dP3Up7`)#zB4?E$78 zB9^|5(6Ppd^mM-=e6)krZvv^$wSx0?XUlHbxTn4|D&QTg zD;=b^Ln_TLLA1WtwWo~0*c8xM+kpRkYl4LbUw^H0jhNy*wt{b{@lkrL(@fx#qnFEk z!tIQfkI`dOJj6q6*-$%^LcyAq)Y3IH=oy6e76C8SF3^APXu^?*xXNxW6ohiu_9Ueg z^Uvoc%B>aN3(JMu2H+RJP$Fv_t1t2lt$C%0$Tert zmnZm!qw4qfg5CTS*quE&0zKEbNS!0~{Nv%9Kk>>PcRzQV++p#gZxVOP%L8= z>Sej>P5hv};Ob-Om6{sJ0BU=2zg*tbHdmT)RX>G(B_r~9A#mhU(u+DA72S{i_;Q6& z=RbLZ>CfcHv?T8^-L$)GcZ`H-KdIyHpvoMj>pned9CQ`Kf!mcv_2wNmXCrtB#9~i; zNg=0<#W_R^-~fF}661^M@vZ%UpY}U}d(Qj%H_`oWrGNRHoepM{Cg(80;Qy+) z4nlVu0_f6tq)wlf`+A`Xvg6!ABt|{x1Qt^%Kug>fuv2bH#ZR6-RThsO;a4JZtZLUi zk@n)j19s~cwe_TDQ-N8aiU$u&!}c_n?x|^|Q>`hR!Ua1IM<>_;EH%ff%GpyMEINbS z3DC7qi+@6zUg^!swe!$qRtmMSN^K=~3vvZ^j^Qi{oYLYD1`^3q$>eH=)*E)dLdRrs zJ17m)P$nw@R*5R!WdS3HC+(RlZA{v^Mi{upoILA7xG!HPDi;_RO~eQ+l@O1%DZD`Y zO_K4d^rC&P{e%b!XnE#304x7vSsF&P#2H2nekprRQZ$UMs1+KKV>wsvo?wzaA_1{x!FHwTHo{LT2BzR*mV!pLDN^O0%(^d}1wb|ZGA8A#ZHX<&hBnx)J z1H@AzN*UJhp-A%r{(I7uwiV88gI*jOATGhGNV%i}K4HR!i6Z!n3P@lbGcuU1zo_~tldIUx;1?ps4YRb`R z3Nl8@%X;2UU5H+ZAJ4s~rF@M)D1jvpjwi9?N-eE(mn@L60hh^Ujo@m zJhOfK#3jPF+@}yqvxY#jy2CvuNwdm?*D-dLleLf3>IJ;Ff4xi>&3P_rSX=nrDA!55 zrSU|d{rj$sPV#ZHNK-!sP6?Ez0xTah4vZfui3_(F+`Gr4<&`M31I?6OpKFC-*MJFrQhE|w;nkSLStv4MGZFE03y?IGbn1(0@y(X z-w!XEfQcq;Ftc{I7cpwl0@QvoP2#Ev6t~#$)!Bgq;T49BkuHD><)Cz|YiCm4$BM&` zjSP1&Nz41Y(OU1>({?k0G*_>$iRruDLaW_i{nSpN_7tqb2yF{t{Qr& z3x;lx?pU`Fs1B#o9jhv=?y^_Xv$7Xgoi+NNBT}e2JA!U~hkyKJ12Kz6N4{5FcJ4Dr zqkPzeTC84FN53?)njdY8rzjo8{&e`^4T3vS> zbY?}`W$(f{rE-s)_l~_rW7!v2-jUtJC|Y~tDhmDrx9-B=!{d!5yCfsOB$?Myf`?RZ zS~!L)Bq~$id6Oi81X_Xg%z$N;`cZcD3gpdF#AOVYj zd8(2Ih~LhgQ_sgQI$c|7OVT$jU|)`cfAtS2Ojq!?`&}-+nvX|q8p~!i4vGH5gzSP` zGCWAZItQ92=G%!hfl=9e#BHHh!TIuLco~3|a>gwfcXP<1Jko{*RR|-zI$mBK0*9^m zlCQp$+Jl1X0Nr??*i-NEn8JzBwsVNQeGGfPpY>IwPxN5R=4cKTs=<#u0@ux z{`s*3_HDz8`dT@kzpCYvhM|k1w={A(py+3K&<+r0?}Jk#u6DusRG)^nzc1LVPF)S* zSOXt~o8##tt$N(pA}e3}T5n)0ueE0r+X=D<-pI=Om%$j$K_KA62vU+)(pDiLrh)>b zag5Fi9Vo_E$Dw1tUL?aq3H3&b5N5st9QL5wp$_%9>8#cv4|UvYAsYiD{=~sF z7>Y6vU)QJJB4A<8snd+xLRGFJ^FEpRu*yY|LngtE$c5Dqm}x=!N)beRl2%ryXD zW66{Bdu}`lMO(Cr!T@AQrPS#P8!4&sEe#si+=7N$>+X%4Fc&2q;Uc_&OU3nT&IWCJ zWKxnJ`p)sUvmkqN`W*4lw4)~i@i=dxEw|G)01>F?jvA-OcwqTZ1?IoA0-O*xUc>fhzHe zWpPbCIQ}LI(src)gr%3zVrhw3O@rfWgYtGo+rV~*(h+Ut;AMp|u z0@?rZ%l?B?;S==!FwM({;{@xxJ!~*&_a>7Y^s%&q=St@_3ClG#q2 z`R?P_$yu5urTaLvRiK$_$7jw%_76>phssP-K2+)Y9(oKZ(AkT{fS4hGi+}C1khjTr zD&S~9MmOc~>ZH414QounDuE>o0N9I(5aw2(Dxzy)4tLbOtJqty(5U-L|j26b;$pOG3UWs+wkovBQp zjY(8NIx(NnN!j#)nLct}hStHZK%q;1!FJ%=hU?43l9dHt<=c+#9a%4KoMtxcp0&g4 z9VySO6_In?s;yq@U?n=wDR^pm#Y(MdMS;0h1GF?^ej>$cZ;DqcV${X#lh!j+vN^(k zQmJg>cpPa**kdL?``&bHxOw}ZLRT#IlS&SY$H2{VD6`agoy5N?#av=3(N(@%;pcZ! zeEm<~s0)aDYm>9WIZV0I4J9R~0$Z@Vuak!~#`#rr zm1DgYMD!w>mC=0!%cbd&@kGmM$@k3HX;ED-!#DA{NV1;`7yI}WYog4`6{=1{PWZFa z5T@SrzC-^CEib9$T*NA3n@(D zk2iOIdQb&(TkpwRk8uJ<@iDP}pvZOfC6)B0w z*rd;~I#o`?Fwrm~F>BNCgBp(AgIDu#>)8{xQ!geVkb&8!t$2zDeK`l$ALgu>)M>z4 z0rFG0@%#5Qh3PQ_%CC8taQit?77;e~+_&E{9=D$|jxJEz1ztr_j zbkq1>vt$NBEkk0*-5F-H0)T^^$s`%2jvA}>uiqZ>rZJ4Jh$8VUAIkVre71rs$5w2e zrE#&|H0`?(wTDg!MdPVR@k;Xj-8_T01JaLDA$7$UgiNS% zODycn`b)btW_TfYSLb%#hW0vJx^CgshA|#}$2i#(58|~{);punjbme{L04~m*TKN& z-m9Yx&O-!#Sal+^0UflU+5qS_1TQ*U%(n0wbO#P=Vx&JX#+h?U+;+f#Wu(85CmPnc#THH0TL#0}?Z=-h0%pD-=60 zy7(HaV9<4_h|3F6Gj)nN9(UtiOxnAXow1Nv%BmAsrF53@^$C4-iQiXT-;?edXtjWA zW&szG)#*5{(BT6s9%l)DFvg@NlsR|>qnV*QioQiCIeLoZT(U}LBH&KPglG)+kz_pX zA#M*5G15xG{bUpiakxlIjHAI~u2|c25@YQqAZ$7h%U*;>yDSU~@^nX6_3X+);4|*D zNJv8n<3vxiG!xPDQ3t1n;(4;Ou9zID_rhK&;{1Qcm+=A7Uxex6`8q4B$Vz`3)L3DG zjzmPrdHTo~oUQY&k8@@a#54 zKMsSz-0o4h4adWDZLN%5A^YX>sz`X+@t#!O$hTQ)RDax$_GQSmQ0d7l;Y1LB=Z`PD zWJr(sN{+vKMk*ZB3y|rbhhoU2$V|Id@t?)&7OQJK*{e5a@`kBx>7-M{xqBOzTOvmN zIZ7tA4M;PLs3&#B0bvA8-|yG-_Fm<4wk1Il48NBgsafyEsYsbNQp2q4;u{L`HwS> z&(r&B>$ov=O%ulbI;esddXv_hhECfmE=W5|ERkzlt{|%)QW$20k>y-?m^`^lBcBxJ ziXW#S#f_qK$~la@as&d%3Ia9?Y@pJQ&BmWb!qjaSrZ zXH;WC_=GP4kamgso;m5^L=w3)`I<-p$ zD1F}Iht3>2;p{2iedbKxSF%QpNXm!iyoA*4s!UCVsds?WJboIHjL=hSRsQ>t0l84Wb{CEAvN&gu~~w~Xdwc^@K%Mph9dJ}s@})tCpd5O zE#%)toMK6M6+Wtt$wj{v=emg?%!*9tf*_Ao2gKBSJTL!y6E1@wL=6>0p^xO}eRDk4 z7sox!H*=ZT^?OXve_I&z-~6DL%(_ccQSs#C&R^3?6v@+MsAvo53GEwdpF|?eTOSfe zT>d&*4k~U!`R$BZ#N>@JrdDX8n^zwXso;YK87S>3KKQB>o#Bp~#FU(gD0afdHOO|9 zqF5kz-3nEQ!yFc+)d@|*SEVt@ShhCCM@ zM1xikN#9&NdNXISg|$mH)Eh>D?M<~h?c)w3AeA4lS-bZZ*s$kAFS+=}z!wG&iME8%FKt;Wb=wfz$PB}njM|cftawb}G3Z@E?S3Tb07=OWDtu>T4&C^;P$2sZ1jSypFZ^^W9 zx4%K&3L|j{y4OmhTCZDa%T&p`{zBe6lTSD$(T~K;d+o&1`}d=^?MW;Rm>6qwC9^iP z7dK&mY$NRYE<2WcF6tPiU6HJcK^;fe@gi<_Z}8MswkLWKA-aVK?Gf1R40f36_0%nj z>9-aP!iE8vBWmIKz=~>@fHz*0(N>UW_yYv$#|gD(XMk~IKuK##u^UaDl}llK*{ zkf3wq99C*0_d1>Dr_keeID2T5yg+W;YX{ZSD@zm510Py7#EP)Z$4fOA1E zM>mW9nG^rB_UO^$XKwt@)2ENtzQq6hI`Kbxxs5;`9kas9ZHgTUZa4*k7#CLb{JCMhC#v0LFN1;l zu@Uwy+m>zmMzyuB`ZDa4M{ExI@0;1~EGHTyX}hTdQD z6uexP$S+6z=w(L@OPKLrE{idU*K%xsiPqu4nrdYZMl`jCYqtNqVch!>9Vx()*gTB0 ztsEr$a+r8b$X`b$WlwVSJ$#+xHTWX8OwKQtVeZVxxSq%{PtTS&?3$?P`&^vQCR8og zgmT2@0zn1jzq^Y@%Q5WFooqU*)RtpU1w};N6(P6c0FNUU(tnL;*x%p3rug3DfQ`{C~9e_-Xz8UwiWC%lZH7od4FUdt-TJY38CK)R)40g93ut2P*+vXcOc` zA*MsjzU;h;%Y-~uW+hyCH>r{|Mt~k9?Cl_2z(;90MFZLZ?_7h5u3j7ZHQZvx(PbU5 zhKG$UAf5YEy(9WN9}$fo8w#DHqodt}S2IlJPQ_CA&*LbkJPmLV>||%TK8gOCT=!ot zvx&EP(0J4>AJa&5{lw2I>%_fA1pOh7J7>{#K8vnIPAo2OLU&qSv70;^2@?u=qYtFp zr1;>!j4B!P#}Iu=l-dF3qBljqOa%A0?Stj6omqTI(T4Vxa^MNAY%Beqs*=tnFHl%~ zJ!2Qm{#x&ePXObl=)>9n`%idP9f=QMI!ac6E`LC2A)ZX|I7Xe}`s3wj+KV2quSfPi z@k@A^O&jQdw@#}WI}Pv93?nQfOJ*36+Ylj%b{h=? z+Y!-ozlp&aI5&>XoP?&qQ9Ea3H-&|`%>h-La;GdqOAJiL7uXtVUiXSrvX!pr&3FWx z#Q23CJ^jGP_^zAVzme^e2U3=ikq1N`^JUW~YQqSH(!0kyV6NBu8OsmG%(O43Sxyz$2&{&VY z{nniO4UaIIpwIR&d7YH#^zzz6zpX`5b?1mNVy>fpo+s4+?+=52VmNT8t1coRcUTBR zUpY?e5ZSVsELsf>?j}pFY{+v-UZmIbBj6+U(2O64&kb>Zyg@HMO z<4pK*(4rZQ(S|B+hS;k};jCDr(=y9BilWFXQG1mkepQ++b@@DZ@@@XQbJkTc4QRas{SPLADa%bB{tT&jKr>#DV03sBLg<-st?lITH6|9KvX^X^K&Q~szT zs*xKyo?W&Jj~u(dv+t6eYJ`7KN$biTK=Q$P{ki227LmK@d_ay1s`q#fQVaTG&UP!&gqOIFY*33$h{L7~&!4S} zWcZ2JgtxG0XG9SP}`wN3?{=9N^$&KdX$xDHo^+8m==#5 z%eYuQ(8a@Vp8fghH_z7AAF@jJG%k!4TfK_QRlRdp(`+@(%hmPAt81(1F}Nxg4GyB! zDqn>w6Z+q+0)&T8*B?Fk^PkuL^auIJg6a!eAm{KfPK(;5%RWl@46)jKa__ad@v624 zu$~WL^O_2C&q+&>eVt+kL3VK5{&AYqRvY4`BWnd6x*T3A%sQmhgRSL+v~QN5L^}{l zh;m5hZctK}KCLtIG(%r;j4RAX@wgOz!OqjA`d3cP@=l$Tb}xrwo!mE=l*xI9VH}r3 zuBB|hDvu*<&zaPZq~j5$78_~zP5V-B=4FJ=upk^eIIz|I2uM$Vf5TnO%ad)x1MZ;*5Txs&*}?SrNE z-#R0_9DeaDYWDxR;B4j(%OOtLD!K{?XCU~A<$t+=(k42ulK{}tDX7{1_whH+zH$72 z9z9xnw*JNc=U4Fm2`#kO75H6#ZsxUbr~$B@%_e1oOHiO&EgNIN#Pwm$O+`jlaW(w7 zqn`rPD%B60Rb0_+m9wIlcwz&I#DN)Z|O%4IOdmKSXX`UKr z;P3Ub;}p}59KUwsRRu~NxpfR|9(0r7da>SLN`3)7A4CP?nbe4)eo&`W-%7*cE(^{I~w(sVo1jtv&i8 z|NTnxpRv%U)c0>vp-E&vzPq|k?J8VZ^Y&d*!i^b|JLczE`rqs)e~U^>7Vw`tS-Q-! zm{ydS@i-K1r4x>bmZqQm?NY5<<26&P2NJ9=B$+>a7;RD+7qV0mlcO*IbkU}u@D&-A zZQ6Y>^t@oF_mVR-Tg*}v0YQbUhy~|_7QJXMi*n3rp&?Nc;!Ba@E{>*SEI)-g9;=H2 zW9nrn_)#0m66zOvX*52rUo)D3h})H&nP%}sAitHg+tz3s(ry38N5ne=q#H8nH?b~N zjwN;4#=hHA8jg9l&yU|&toxULcOw7QkGDAj;T-wz$)hK({P+0D+Ose6->)tIdF7aZ zJk&;Xsli()U9(T@nIqpE#>IJ3(UBJMx!ljvv91e4`9TrjxID~f!;7S3yNHp;l-i7h zi4Nr#l7IXnQ7xZvZNSpjrz5z}-6p7&sh`jf>J1q^I{wwegLDF7QJf|DtQ=F__wU-m zCn=@ALI#JYJdmI77~i1vO? zWAr25AM_CB;LUzYr&u!_-N6{DZz5zN!Jc08(D-^nfJLDSL=Bkey)zOY&%G_ZHSK6> zgpIRwbK|=btz6c_fCRppXR9JrlqrT^#3d|Ql10LY3B~&KLp%b}zD!Z-7y0&jz4od2 zq@z*Kay05$ac(=KG@p3F+~_&9VkB{D%u9R=72Y z%yaEi(%l|)?0P=(wLBE25yv&(2*b^#s-t%)GDg=qLA&b==D|EQ(GPaAH)&B;`n1@(h*OHx zk&Y)O1CUPylM)gcSA1SlZm#BAJ5L^IPt4c$VQgZ`SJdMgM-xR(sJMwevKMB@-MRym z@wTVZ2T~%y=23|G0G$^3w<|0S5_&t>;DmWhwsZc;~BoY}L2;0n=JmMNCRQkyrB~1u3K$ zUR+ar2#at-<;E<01a(?)*0-!^nCR+ekdV69-SF*DS!=?8Vx&PhRq|IQUdd=b zIZv^Wj0`&G7E~-ych0eh#M?cnMFXE>>PD!s$5k`f*6U6hZ0+~Vl^XcpfCq@9&izoN zm(sN1@eE<8sD-U$neHgOc*+9fUp(>Ja6{36@6-=PlV1xzl*msz(ep-+Rr=0DMr}Ux zQdFA{{lqo*ON>o=@L(x=z?VlEK6p{&SVA%5Fm1{;k(|!XDSjG%k-sOFyNc5>U0qQO zqf4zJy-A8*LGUqFb>N~8n1O)mrIx@G6B%mFrMx9ekU0!YaQr1zlz}_!gNH~eO*#?% zK-wC1Dhi*6p{?H42!I#k8mxfaB?&!hFG(w#2VMO?|V z#L}0O9BCQt7UyHpc`p)W1bg!;mFj(BjiQKs?#R%mo;If@HvKL0&wFd0z4iiNT8p8(QI|8+p`S`emcisUTj6 z24i_OF+yK3cL1;#+coFxmI`GH?8V@Ir~#-IP*40R5me-~)X(Vlt!)Tx zEC>>6w_R@ycdxm9hrhKk>6OM2=T}@ig880My6OFawW#0cm&swy5!z^klj#Dx8Crwz zZ`1X(_JwMtG9joZZq+K{8}pd5R#izPN9oAy(Zt4irBD!seL3Hug~a@ehOb6e)6JC- z^~J&Yt^lSRRp5U4IS9~mH?ZJ&ub^RY-aCT@FmO)|gY@4fESL?qS$EO{uE?4)I28?D zy80a!&>SsAzt3;`TpuvH-=!OyDIVwJ(P9wf2Q;i1nfVNDiB#HWt2FS*;i(8kvr>4b|v;(Z9F~i{TKOxi;cIX+O6%c z(NyF0`}gmxaN|zP?nJGjkcvULHGK{b*LQ9Aj@Hvq4Yu{&UySw83?J^a>+9Ad^=lfu zWA!#wqg_B{A;!CJhje$^_IT?t2b3hqKeZ1hMn0A0Y=S~Al^TWXJ6$QxZ)CZ0;=##7TuMz;RW@n z%7A*;PEk{nPSjIO+_-^wZ;i*MyYYXX(o4{|OR}0SZPKPu(RIb5Xz8fd5Go}uGPdxJ zcIO|lR3JLio+m|@9+=W>sKX#I`d=Gqu^7K3+TkwSAZ~kmV-&#fdN@u-{xWvIiI_r< zDe89^Q`kCQ(3q~qc)^gp`AhKNFYW(j)c;G+bMaMN+@|bbJO7WzYfqm&a_j#+S$p>M zi~q+j>HqQV$DO~X=nw$^Yq=aW4w=0cWwYM4|ZGYhLY9Bb^d7R6^3=YmG>9F*?H=`gK?7>+_F zDYIdQddg=bE%#2#AqcOpfI*!XWZO%s4deY=IHYxAP^^`eRJE$nLo>6hH4}@*lj^9^ z+q)LXH8Xt*8oUZjmkc1~NuFsBig~FyjP`+=eS@K${nkT6P7-;3zn_$VWNBpB#MoNQ zbUl`rCm_+o@AnRTWJ$r4LoOr=n0o>C?i9OvGH($lCQ~w5o%Kq^+-lLk*wRm*+HJ*8 zpW;1P3E?${4X_ZzC9uWKd_d0lxXp!vK+%hQZn$J%d%Cb4!iGJKDPHe9XmCt`f9+*& z^5Lu`ex_}qiNW4T#Q~D#v??^!15uAynw_IMl-QaK^M&0(SKZlGU*s8oK}dDx!0Gq* z%;rCR?$+!EJj7SyS@ByNA>!12fY>@bKG9}YRk*v8l9!W$-hr)KCGC;V|CVM!fu=ykv5NYm^q*`HRlhk5J`ANzL;}#@UTc@9oq%09q6wRoKmPABeiL>@>!|4Mh z&8>KvR-`*eZ8CCJd^(g*xe^Xup<=5ss~NpAbtE9yVf1Yn$1Xl30$=1~icK~StEqlI zr(Y9-@l~xSXnqLC7~PO@+~V5YJvE+(%Aor%68sm=-{HipeC_kwnskCTQkKkn++)n$%VPqBV- zUUn@3OKVD(AZI-V-vPI#h%fowz6bYl%=>4jNFLN(Kv#hS5gV5Wo*aC{Y()MGy!oVp*Wy@ju8`;_V~4HWpHm8_WW;WqZYGo!&d5jqgPiv5A+)Bgl;W> z4=WI@`-Gmz8YUr4s((eWivV7xZhLE-l5Ej>KIWJM<{?uj?HYRK%(g-=+$UTRUoJ0i zm_gy`c^0H5&$_y6;8o<;2=azYRWIEu_>G*SQy|8s+h$;VraLWel_n5@4QIx@jT7Ec z^ArF$VwisW>ncA#A6v65bV+C9_eff-#B zjeC3LKu_~#No7%mdDplm87I=}YQ+95Q8`V9=~+4qM6`J0PuPkg;#iE}mBz+a$cht) z!sqx6B10VKg>d-EUG-28Ix!&k>RL_q&u?sF$EJBzsq;V_yTJoyYgduv!O3k>L#d|V z0#Quv3kngSzjo8{&e`^4T3vVMbYINOE-588Hv8s?5=!&srH$;d&$%+u-iKWFJ>JO(kM-`MSOO}O?9MXBOFqfqZ^ z1JJ5`n0TS6r+KGCfPQv@z#7THv_g1LGGEuQvdku}5wk!x;$~lgoZ-oL^|?~lp7wL4 zfX&}Jq=XD?98;S}(J7{Pmq~T1zIOd$JeTMc%tl;8eqx@z<8Nt=BkXn74w%A}JUTN= zH=wRz0PQXg{ODi*T2gVp3ol>r>swFg#==2(GTPohT^8Z@oJLY3qQqGEjH97!#f(`4 z?6(nrqXhJD;EgiUQAi!Sy~gOa!@{L;pBVC+-FT8jadBSiCYwbOUqcIgN+<)Xunt`l zGI2@!IQ@DBZ<6>9ov<@Q z>Bc}&(G*Xo@Dv-w6vroGBVQ*Krrz=f7Gbg0E8v2wFc9>`w*rh-rT;ny8m&M5Q!PB} zOT?2&prKGFrB+a3O^x3C@8c{!$IQ8i+V=ncu(y9Wc)$1h0E9A3hcIBvR)a=yz zX9?lWI6g1GOR}Vh$NVIGk7bZsaQvJCcj^EAr=}|};akE4W#XB4Dkd}WzK zg8y{>uHe7VOHFT&j6QjoG+QFDgu?BKNZ-I(N+P0MUk1r|2;;Z`rO(HAwR{H&h&rRV zxaPeGLLZzozw5>HY}_I_11al5%@xnmFS9_eQ@|*`exHw$FMG_l$BYU3P42M0B9+I~ zoi5;!_W+F&tRXW@;4*{-6uFq2fx^5o>wJSRPvNVx=*=wSvU!x01?|w7h7eGMOGXJY zJ>y);G=n=;gsx-U0;4;*nqm|<5(%iRH&bxc$0*LOqany(`2?d(p${xRgIh1HxLS6O zB^M`2B|CkIIaVf-TKYo{9pxW6rvcUrsgj`3g!BvmzP*2VVMUCBpq>v?v{(^=afV?@ zibnF`tBVxwVMQ`b;DS=hS;CQEe-jerOZr;)egg?pbR*|cG{lwgVRCb+#r)^&lf{G= zad$nal4-OKH@$O~24q|>W>@ok)*IIgYC1axWWS^ZJ=!u#IYRNVaC_t7Z_@qaKpgWjszt zRE7+_Xl9d)?TWzhU?j?BD_Q}k#wJ_31&;N73Vd=@U8n=1H%YT}GMjwh;qXV)WWF^9 z|4rf_U4ZeAet@QT!p)F8m`&w<=PpqVfklK{yK>gOu3D8%z0Gv&WN0sklX&5&XqXHi z(3+)2)8!+eOLEIXs+9a4jwUA1QcS{pYD3pGRldx(nZjQhyD8Yn(*4gT)%n;2zcjH4 zHS%MlSaSV?z2c3m+l!bXtu6T3x6yjjcp88x5YHFv({T8|**I^C@y#$@3eakZJG7

62_AE)zTIrxZY+LD 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 j*Eon{zvG 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ⅈx)1Si5be%n@*xly-+@F2{ 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 diff --git a/apps/mobile/deps/react-native-screens-5.0.0-t3.8.tgz b/apps/mobile/deps/react-native-screens-5.0.0-t3.8.tgz new file mode 100644 index 0000000000000000000000000000000000000000..bcb0e35191fcdbb6451b1a541eedc56f258c92c5 GIT binary patch literal 768223 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Ȅn2Vs2NaqPjLtW$L~1 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 z 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

_1g*kMw#EgD_Pu;6dd@wM2lq;?O7 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�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�O0 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

)Lluo}{pOcULb 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

)n85i6nkHdo)8(uH zgefBGSQapr?ZA^nXl70hO??zy8Bp^za!R^d1{2tG-F=s4t#}#^8f?ltF7d}$3lLRN z;H!+415X14+&GVxq8_+bWDHZC5aV%MVT%BA#DfV#7;B=6M@pEW5&=&{*&gQj8iv}p z%uYqL5**EVRCQR6=m1dbVOFN^1`{gQkTFv_W60J5y@OM!d{L((9K5qmW_iUfoKUS$9-O=kPOcWG>0B}B zrwJSZOJ-FB=HdvjGWA}9W0_wi6Za1z8PPSZNQdWI6@X(5(^eWIU*hm$AmaYdQ?=*Q z4&IYzI$;(0p0Px#o%oo3khb6M^szNI)(86#^Cem^s7PTm__##rjj%g8>KkBSHLg*o zKv7G0Y*MQ4iQ-TsoX?H*@c;hL{{VP#cTY@D3^pJgFOV49luwOm*Y}YDTP9w7HgtSVU7IAK-ptm9v>sH5j;FwWI zahyQdLOeG01vHP)tf*1h?1~!WS^BSY-*)65F@Z5|18fLz(p%Yz`j+dBK{?z{Fl3d73_tIZ%}?lNTeS?#m*W_m6EL&303Eyzc1F3I z$TR9SpvcdaPfy-32JSe_b0^qoJaazpqLNZ>60jkY{Ulm$HOgGO$!(4k^%D%X2tP!I z^C_|y%wzRo7Ea@oD5cE0EX{Ka9ulmnImU!6uhXS6m%$}d?1IM{1-*a{Bm^8vLY@<| zy3)VEs3o4$FtH3gjx3r)@yB{Ut%Cz_Cx|}cjVVe@^hwE&dXqYXS;VK}i6~OR`*m!M zrlh~P2vJEF3UCN}1({iz+85uF3dCIV?D*(cFxgmRfzdC^4z z7F4_`9~B+LJHe|JpoPNgt7ehoilR?3S|n=l7+vx>vlap8Lb^~xEF*lo($`Ar>%+UP z0owk-*`9G(Ial~91Km->kGWD z8FTF!Z{2LD^#$t(zCtnH%OagdJP$<+TtAwIOXT#4GV(&D=Y3;Ej$%UTFPrClUbiYJ zc4CbMZ<|thXsQ<$btURD3NpZgG*7{N;H>tV@7`=b)p&&3Da>6 z%RSNTuRZ5_CuQG+ELbIF$RcQ$RGLAZ0*Pfj(;++&KI^}*AMzEuAkT?pc04*!$7)1e zRbhmqr;V4K{-kBQ$ZQY4BhHyL>Z2)wqQF^0ED3^byz6*wrt3{?=2VoT_N}C~EMO zQrzx%>{ikq%97LHsjgHLBq0$o2=D_y$?9%bZjt6kubuWf@HYQ;zARxBda+)%=p3H!>bF)081B zv5h2ycT%-L^K~493=FNFKBrBB&TS5fOjmT*XD{>Z#JoXYmibX|u`yv8oYQm}hl$cH zhM7Yyh)ejLOIk8D6=xcGNx01x8`ain2>o1w=o&jAoLRnl_f1Q2;tg2x7psRt19Rv+ zTe;wcXk(ixmg3rx>uFMhSxX|5uqdzQi|a)JVL%G1(a`0i4NeeUr1%mXBCXRS!^8IE zV(`(bK82zk+E!@HKnKy`tW}-z5WVcl$>u=UL${Bm9#d<9sBNN)SHl11R&>Sq-{YgZ z508uSzYia7@P8Zp-+vzduN44LNEhwFfAY1Q?9empZ$;3b9Ay3mru#Q*b1E)GTcBTM zXEuOevCC%QzUq!DEltHPI|KS;L#u=ND(6!R;ww(6HGr>_mivY7tqZuPho4jU%GR_V zu2+5l=i( z8U!zY%rfx1>dSQn*j@1$t^~QO@BSl^0sU z>WZY+98y3&xbz*taHY$?DhzIA5L`}u+gdmLts84;19{tE-Zq%G z4d(4D$Go+)N47Rau7Y%{#<{J{*4WX^*uliuzjd(>3|pmHrC;<~AK0}<$Tg{)^@>_s z8MD^fs@52()>!N}p;Wgo^nYRUem{t`a{EV5ShPNnXkBdBP4H(;7PTq%tf4U99d*__ z=FBSO_lGuXFVk-|LztO6+yGr>(UfbDWu;r$Bc`l3L|G+oX^bRmV@_y{A}d|T9`R#6 zqR0Bfj;#+lR%5(a6*0DfjM*V$l=i$heCyW|IJUulb;$qbaydd14tBO2W7Wr@_{ow4 zt}g%M@%=*n*SnkiuU|_3=S)^evDp=QpY!A(flY#Qz4-69FW#NLdhz=F$q&wxx2FT= z)#-QNpF7XKfAadf7tW6dZ=5Hu|F`q{&H3rG7sLXl-XuUqYfBX^I+gV{CDF+UjhZM%m4nM zQ2*)v;loY-=P&I1w_yNWhR9$7w(3TL5#&l8)ppdJ7qnmo?O4H9H%`zCAGn?S|H2)| z{zR~W;y}gVauv{%7aIAN_RpoVA8WmGd+zSUk2$0=11*&ThbydyIx5 zsU%IJFiD-Cg;H~p-MmM}JRcVnAo)^bQKNzhY$EKw#`wQ(On=q-pGSA^75M+%!@GwY z{(l?)wh@y&34GW|l$bKt0kAB~g#h1XuLSr)G0VYL$*rK|%n7%cqoPuwbM4Y*kM~NK zi=~HarTS8#d#CNqLNC!2;V-&i^0$S38#6b|4i;~D_xh}qlWb!@zls!|V#yGkb>bkj zjR)agV@|M1>% zq5k91@!^L5eVD} z1l~oupGNz!e>qDl-;2}0c#odP7qNlDdvz(~e#ozU?8 z8=w>sB?}`qPUr|$Wt5~hfj3mWj$~^)*E4@ILmXLRZmU#&Mm#-5?<3*{s>|J>bDA1e zMDTm}(#64F`7Skx6k&!sCXumu7obih4gt4B=!P1hY?2UeA)&(rdkJ<&yJ zhA-jF%4Nxm6+GH<>^mHu333=GVHQK7S>qe=gM?U7P(ygb2Sqlw*7HIk(MoM4!eB7CHEyr2*CAUG=Ou*aL~Ybw&%#9Cjpk{{UYU)()>$!fV=?~x_TOege+$LGnf!nF zFfacfJ$!g?jWC%fpzzstH5&=zZ6oI+}(M1W^Th|$ABK)@z{hJH^R&sAI@>@uK6>+~$ zVZT37zk#5?3VnAF?l;o&f2{rg+8a0b0oLJv4-5E@qlfp8H}e0Nh5yY+v6I;ILLvuK zwEsr8^5+~RO_9!tBYk;q=6+JW=IyTPQ7HUsw~9_G#+b4+)3hxoP>P;-{-Pbk3l z))+-$$`;^%Uq@4K1y7?qKA+#}(H#F<)XDns|H1jcU-bVSZSue0=J_|2d>htxU;%

905W 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 i%t`EZowOAjKjU^!(OoGUxw#sqQ_<@0xY}* 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 + private let shortcutInputView = UIView(frame: .zero) public override var canBecomeFirstResponder: Bool { true } + // This responder handles hardware shortcuts, so reclaiming it must not show a software keyboard. + public override var inputView: UIView? { shortcutInputView } + public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { if action == #selector(openCommandPalette) || action == #selector(paletteNext) || action == #selector(palettePrevious) || action == #selector(paletteDismiss), let input = window?.t3FirstResponder as? UITextInput, @@ -89,6 +93,8 @@ public final class T3KeyboardCommandsView: ExpoView { public required init(appContext: AppContext? = nil) { super.init(appContext: appContext) + inputAssistantItem.leadingBarButtonGroups = [] + inputAssistantItem.trailingBarButtonGroups = [] NotificationCenter.default.addObserver( self, selector: #selector(reclaimFirstResponderIfAvailable), diff --git a/apps/mobile/modules/t3-native-controls/ios/T3LayoutMetricsView.swift b/apps/mobile/modules/t3-native-controls/ios/T3LayoutMetricsView.swift index 43070a614582..4e0fcf6d2206 100644 --- a/apps/mobile/modules/t3-native-controls/ios/T3LayoutMetricsView.swift +++ b/apps/mobile/modules/t3-native-controls/ios/T3LayoutMetricsView.swift @@ -1,10 +1,12 @@ import ExpoModulesCore import UIKit -/// Reports the body geometry of a native navigation column. +/// Reports geometry in the observing view's coordinate space, including system +/// reservations that safe-area rectangles cannot represent, such as the fold. final class T3LayoutMetricsView: ExpoView { let onMetricsChange = EventDispatcher() private var lastMetrics: NSDictionary? + private var hasHinge: Bool? = false required init(appContext: AppContext? = nil) { super.init(appContext: appContext) @@ -12,6 +14,22 @@ final class T3LayoutMetricsView: ExpoView { registerForTraitChanges([UITraitHorizontalSizeClass.self, UITraitVerticalSizeClass.self]) { (view: T3LayoutMetricsView, _: UITraitCollection) in view.publishMetrics() } + #if compiler(>=6.4) + if #available(iOS 27.1, *) { + hasHinge = nil + addInteraction(UIHingeInteraction { [weak self] _, update in + guard let self, self.window != nil else { return } + // Capability survives temporary detachment and does not follow the hinge angle. + let hasHinge = self.hasHinge == true || update.hinge != nil + guard self.hasHinge != hasHinge else { return } + self.hasHinge = hasHinge + self.publishMetrics() + }) + registerForTraitChanges(UITraitCollection.systemTraitsAffectingVerticalBarEdge) { (view: T3LayoutMetricsView, _: UITraitCollection) in + view.publishMetrics() + } + } + #endif } override func didMoveToWindow() { @@ -31,14 +49,43 @@ final class T3LayoutMetricsView: ExpoView { } private func publishMetrics() { - guard window != nil, bounds.width > 0, bounds.height > 0 else { return } + guard let window, bounds.width > 0, bounds.height > 0, let hasHinge else { return } + if hasHinge { + T3WorkspaceOrientationSubscriber.allowHingeOrientations(in: window) + } + var verticalBarEdge = "none" + var regions: [[String: Any]] = [] + #if compiler(>=6.4) + if #available(iOS 27.1, *) { + let rtl = effectiveUserInterfaceLayoutDirection == .rightToLeft + switch traitCollection.verticalBarEdge { + case .leading: verticalBarEdge = rtl ? "right" : "left" + case .trailing: verticalBarEdge = rtl ? "left" : "right" + default: break + } + for kind in [UIView.ReservedRegion.Kind.division, .occlusion] { + for region in reservedRegions(kind: kind) where region.isActive { + let frame = region.frame.intersection(bounds) + guard !frame.isNull, !frame.isEmpty else { continue } + regions.append([ + "kind": kind == .division ? "division" : "occlusion", + "x": frame.minX, "y": frame.minY, + "width": frame.width, "height": frame.height, + ]) + } + } + } + #endif let metrics: [String: Any] = [ "width": bounds.width, "height": bounds.height, + "hasHinge": hasHinge, "horizontalSizeClass": traitCollection.horizontalSizeClass == .regular ? "regular" : "compact", + "verticalBarEdge": verticalBarEdge, "safeArea": [ "top": safeAreaInsets.top, "bottom": safeAreaInsets.bottom, "left": safeAreaInsets.left, "right": safeAreaInsets.right, ], + "reservedRegions": regions, ] let next = metrics as NSDictionary guard lastMetrics?.isEqual(next) != true else { return } diff --git a/apps/mobile/modules/t3-native-controls/ios/T3NativeControlsModule.swift b/apps/mobile/modules/t3-native-controls/ios/T3NativeControlsModule.swift index 3749badcd5ea..32f5df0f50a5 100644 --- a/apps/mobile/modules/t3-native-controls/ios/T3NativeControlsModule.swift +++ b/apps/mobile/modules/t3-native-controls/ios/T3NativeControlsModule.swift @@ -13,7 +13,9 @@ public final class T3NativeControlsModule: Module { public func definition() -> ModuleDefinition { Constants { if #available(iOS 26.0, *) { - return ["supportsWorkspaceColumns": NSClassFromString("RNSSplitHostComponentView") != nil] + return [ + "supportsWorkspaceColumns": NSClassFromString("RNSSplitHostComponentView") != nil, + ] } return ["supportsWorkspaceColumns": false] } diff --git a/apps/mobile/modules/t3-native-controls/ios/T3WorkspaceOrientationSubscriber.swift b/apps/mobile/modules/t3-native-controls/ios/T3WorkspaceOrientationSubscriber.swift new file mode 100644 index 000000000000..8fa0a232d100 --- /dev/null +++ b/apps/mobile/modules/t3-native-controls/ios/T3WorkspaceOrientationSubscriber.swift @@ -0,0 +1,22 @@ +import ExpoModulesCore +import UIKit + +/// Keep ordinary phones portrait-only while allowing a hinge-backed scene to rotate. +public final class T3WorkspaceOrientationSubscriber: ExpoAppDelegateSubscriber { + private static let hingeWindows = NSHashTable.weakObjects() + + static func allowHingeOrientations(in window: UIWindow) { + guard !hingeWindows.contains(window) else { return } + hingeWindows.add(window) + window.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations() + } + + public func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { + if UIDevice.current.userInterfaceIdiom == .pad { + // Keep iPad orientation policy, including portrait-only showcase captures. + let orientations = Bundle.main.object(forInfoDictionaryKey: "UISupportedInterfaceOrientations~ipad") as? [String] ?? [] + return orientations == ["UIInterfaceOrientationPortrait"] ? .portrait : .all + } + return window.map { Self.hingeWindows.contains($0) } == true ? .allButUpsideDown : .portrait + } +} diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 0efb5e01a11d..d799d467a0f0 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -127,7 +127,7 @@ "react-native-nitro-modules": "0.35.9", "react-native-reanimated": "4.7.0", "react-native-safe-area-context": "~5.9.1", - "react-native-screens": "file:deps/react-native-screens-5.0.0-t3.7.tgz", + "react-native-screens": "file:deps/react-native-screens-5.0.0-t3.8.tgz", "react-native-shiki-engine": "^0.3.12", "react-native-svg": "15.15.5", "react-native-webview": "14.0.1", diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx index 86166278108b..9be57907b358 100644 --- a/apps/mobile/src/App.tsx +++ b/apps/mobile/src/App.tsx @@ -27,6 +27,7 @@ import { useUiRuntimeMemoryWarningGc } from "./lib/useUiRuntimeMemoryWarningGc"; import { SubscriptionUsageCoordinator } from "./widgets/SubscriptionUsageCoordinator"; import { VoiceInputProvider } from "./features/voice-input/VoiceInputProvider"; import { GlobalVoiceInputControl } from "./features/voice-input/GlobalVoiceInputControl"; +import { NativeLayoutMetricsProvider } from "./native/native-layout-metrics"; import "../global.css"; @@ -91,11 +92,13 @@ function AppContent() { header (glass buttons, title, materials) is forced light even when the system is in dark mode. */} - - - - - + + + + + + + {/* Anchored-menu overlays render here — in-window, so the keyboard stays up while a dropdown is open. */} diff --git a/apps/mobile/src/Stack.tsx b/apps/mobile/src/Stack.tsx index 714eded82d68..ec4b2565e174 100644 --- a/apps/mobile/src/Stack.tsx +++ b/apps/mobile/src/Stack.tsx @@ -128,6 +128,7 @@ import { } from "./features/sharing/incoming-share-presentation"; import { NATIVE_LIQUID_GLASS_SUPPORTED } from "./native/native-glass"; import { deriveLayout } from "./lib/layout"; +import { useNativeLayoutMetrics } from "./native/native-layout-metrics"; import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader"; import { FORM_SHEET_PRESENTATION_OPTIONS } from "./native/sheet-surface"; import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain"; @@ -192,6 +193,9 @@ const LEGAL_DOCUMENT_HEADER_OPTIONS: AppScreenOptions = { presentation: "fullScreenModal", }; +// A navigator container leaves horizontal safe-area handling to its leaf screens. +const NESTED_NAVIGATOR_OPTIONS = { nativeContentInsetHorizontally: false, headerShown: false }; + const SettingsContentStack = createV5SheetStackNavigator({ initialRouteName: "Settings", screenOptions: { @@ -410,6 +414,7 @@ const SettingsSheetStack = createV5SheetStackNavigator({ screens: { SettingsContent: createNativeStackScreen({ screen: SettingsContentStack, + options: NESTED_NAVIGATOR_OPTIONS, linking: "", layout: ({ children }) => ( @@ -845,6 +850,7 @@ const RootStackConfig = createWorkspaceStackNavigator({ screen: SettingsSheetStack, linking: "settings", options: { + ...NESTED_NAVIGATOR_OPTIONS, gestureEnabled: true, headerShown: false, }, @@ -911,6 +917,7 @@ const RootStackConfig = createWorkspaceStackNavigator({ ), options: { + ...NESTED_NAVIGATOR_OPTIONS, gestureEnabled: true, headerShown: false, }, @@ -951,8 +958,9 @@ function ScreenRenderFallback(props: RenderFailureProps & { readonly routeName: export const RootStack = RootStackConfig.with(function AdaptiveRootStack({ Navigator }) { const { width, height } = useWindowDimensions(); + const nativeMetrics = useNativeLayoutMetrics(); const usesWorkspaceFlowScreens = - Platform.OS === "android" || deriveLayout({ width, height }).usesSplitView; + Platform.OS === "android" || deriveLayout({ width, height, nativeMetrics }).usesSplitView; return ( ): MailMenu["it } export function ScreenHeader(props: ScreenHeaderProps) { + const usesNativeWorkspaceColumns = useNativeWorkspaceColumnsSupported(); const headerId = useId(); const { layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); const { themeVariables } = useAppearancePreferences(); + const usesNativeMailSearchToolbar = useNativeMailSearchToolbar(); const { search, menus } = props; const menu = menus?.[0]; const compactSearch = search !== undefined && (search.compactToolbar ?? !layout.usesSplitView) && - NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED; + usesNativeMailSearchToolbar; const visibleMenus = compactSearch ? (menus?.slice(1) ?? []) : (menus ?? []); const refresh = search?.refreshInToolbar ? search.onRefresh : undefined; return ( @@ -88,7 +92,7 @@ export function ScreenHeader(props: ScreenHeaderProps) { : undefined), }), ] - : undefined, + : () => [], headerSearchBarOptions: compactSearch ? undefined : { @@ -116,8 +120,12 @@ export function ScreenHeader(props: ScreenHeaderProps) { onPress={props.backInSplitView.onPress ?? props.onBack} /> ) : null} - {props.sidebar !== false ? ( + {props.sidebar !== false && + (!usesNativeWorkspaceColumns || + (Platform.OS === "ios" && Platform.isPad) || + !panes.primarySidebarVisible) ? ( >(); + const { panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); + const usesNativeMailSearchToolbar = useNativeMailSearchToolbar(); const primaryColumn = use(NativePrimaryColumnContext); - const iPadSidebar = Platform.OS === "ios" && Platform.isPad && primaryColumn !== null; + const sidebarHeader = + Platform.OS === "ios" && + primaryColumn !== null && + (Platform.isPad || !usesNativeMailSearchToolbar); const searchBarRef = useRef(null); + const focusAfterReveal = useRef(false); + useEffect( + () => + navigation.addListener("transitionEnd", (event) => { + if (focusAfterReveal.current && !event.data.closing) { + focusAfterReveal.current = false; + searchBarRef.current?.focus(); + } + }), + [navigation], + ); const iconColor = useUniwindTheme()["--color-icon"]; // The list uses a fixed creation order and ignores sort/group options, so // the filter menu only carries the filters and the "customized" icon state @@ -27,9 +45,14 @@ export function HomeHeader(props: HomeHeaderProps) { const hasCustomListOptions = props.selectedEnvironmentId !== null || props.selectedProjectKey !== null; const focusSearch = useCallback(() => { + if (primaryColumn && !panes.primarySidebarVisible) { + focusAfterReveal.current = true; + togglePrimarySidebar(); + return true; + } searchBarRef.current?.focus(); return searchBarRef.current !== null; - }, []); + }, [primaryColumn, panes.primarySidebarVisible, togglePrimarySidebar]); useHardwareKeyboardCommand("focusSearch", focusSearch); const filterMenu = buildHomeListFilterMenu(props); @@ -42,7 +65,7 @@ export function HomeHeader(props: HomeHeaderProps) { // (GLASS_HEADER_OPTIONS). Only dynamic values are set here. headerTintColor: iconColor, unstable_headerRightItems: () => - iPadSidebar + sidebarHeader ? createSidebarHeaderItems({ filterIcon: hasCustomListOptions ? "line.3.horizontal.decrease.circle.fill" @@ -62,7 +85,7 @@ export function HomeHeader(props: HomeHeaderProps) { ], // The keys below are set per-branch (not `undefined`) so a later // reapply cannot clobber options owned by NativeHeaderToolbar. - ...(iPadSidebar + ...(sidebarHeader ? { headerSearchBarOptions: { ref: searchBarRef, @@ -78,7 +101,7 @@ export function HomeHeader(props: HomeHeaderProps) { }, unstable_headerToolbarItems: () => [], } - : NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED + : usesNativeMailSearchToolbar ? { headerSearchBarOptions: { ref: searchBarRef, @@ -121,7 +144,7 @@ export function HomeHeader(props: HomeHeaderProps) { }} /> - {iPadSidebar || NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED ? null : ( + {sidebarHeader || usesNativeMailSearchToolbar ? null : ( (null); const insets = useSafeAreaInsets(); const { fabClearance } = useAndroidControlSizing(); + const screenMetrics = useNativeLayoutMetrics(); + const contentSideInsets = usesNativeWorkspaceColumns + ? (columnMetrics ?? screenMetrics)?.safeArea + : undefined; // UIKit's column safe area already includes its bottom toolbar. const iosBottomClearance = Math.max(columnMetrics?.safeArea.bottom ?? insets.bottom, 24); const searchEnvironmentIds = useMemo( @@ -908,6 +917,8 @@ export function HomeScreen(props: HomeScreenProps) { style={{ paddingBottom: Platform.OS === "ios" ? iosBottomClearance : Math.max(insets.bottom, 24), paddingTop: NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + 72 : 0, + paddingLeft: 32 + (contentSideInsets?.left ?? 0), + paddingRight: 32 + (contentSideInsets?.right ?? 0), }} > @@ -1038,7 +1049,8 @@ export function HomeScreen(props: HomeScreenProps) { {...scrollGateHandlers} scrollEventThrottle={16} contentContainerStyle={{ - paddingHorizontal: primaryColumn ? 8 : 0, + paddingLeft: (contentSideInsets?.left ?? 0) + (primaryColumn ? 8 : 0), + paddingRight: (contentSideInsets?.right ?? 0) + (primaryColumn ? 8 : 0), paddingBottom: Platform.OS === "ios" ? iosBottomClearance diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index 801d5e85da80..ee2574fcee04 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -66,6 +66,8 @@ import { NativeWorkspaceInspectorContext, } from "../../native/v5-workspace-context"; +import { useNativeLayoutMetrics } from "../../native/native-layout-metrics"; + interface AdaptiveWorkspaceContextValue { readonly layout: Layout; readonly panes: WorkspacePaneLayout; @@ -239,7 +241,9 @@ function AdaptiveWorkspaceLayoutContent( ) { const projectGroupingMode = props.projectGroupingMode; const nativeWorkspace = use(NativeWorkspaceModeContext); - const { width, height } = useWindowDimensions(); + const windowDimensions = useWindowDimensions(); + const nativeMetrics = useNativeLayoutMetrics(); + const { width, height } = nativeMetrics ?? windowDimensions; const pathname = props.pathname; const navigation = useNavigation(); const activeRoleOwner = useRef(null); @@ -256,7 +260,10 @@ function AdaptiveWorkspaceLayoutContent( const [primarySidebarSearchQuery, setPrimarySidebarSearchQuery] = useState(""); const [focusedAuxiliaryPaneRole, setFocusedAuxiliaryPaneRole] = useState(null); - const baseLayout = useMemo(() => deriveLayout({ width, height }), [height, width]); + const baseLayout = useMemo( + () => deriveLayout({ width, height, nativeMetrics }), + [height, width, nativeMetrics], + ); const layout = baseLayout; // In split layouts the sidebar IS the thread list — it renders on every // route, including Home (which shows an empty-detail pane instead of the @@ -269,7 +276,9 @@ function AdaptiveWorkspaceLayoutContent( viewportWidth: width, preferredWidth: fileInspectorPreferredWidth ?? undefined, reservedLeadingWidth: - shouldRenderPrimarySidebar && showPrimarySidebar ? (layout.listPaneWidth ?? 0) : 0, + shouldRenderPrimarySidebar && showPrimarySidebar + ? (layout.listPaneWidth ?? 0) + (layout.listPaneGap ?? 0) + : 0, }), [fileInspectorPreferredWidth, layout, showPrimarySidebar, shouldRenderPrimarySidebar, width], ); @@ -470,13 +479,21 @@ function AdaptiveWorkspaceLayoutContent( ); const renderedSidebarWidth = useSharedValue( - panes.primarySidebarVisible ? (layout.listPaneWidth ?? 0) : 0, + panes.primarySidebarVisible ? (layout.listPaneWidth ?? 0) + (layout.listPaneGap ?? 0) : 0, ); useEffect(() => { if (nativeWorkspace) return; - const targetWidth = panes.primarySidebarVisible ? (layout.listPaneWidth ?? 0) : 0; + const targetWidth = panes.primarySidebarVisible + ? (layout.listPaneWidth ?? 0) + (layout.listPaneGap ?? 0) + : 0; renderedSidebarWidth.value = withTiming(targetWidth, WORKSPACE_PANE_TIMING); - }, [nativeWorkspace, layout.listPaneWidth, panes.primarySidebarVisible, renderedSidebarWidth]); + }, [ + nativeWorkspace, + layout.listPaneWidth, + layout.listPaneGap, + panes.primarySidebarVisible, + renderedSidebarWidth, + ]); const sidebarAnimatedStyle = useAnimatedStyle(() => ({ opacity: Math.min(1, renderedSidebarWidth.value / 80), width: renderedSidebarWidth.value, diff --git a/apps/mobile/src/features/layout/createWorkspaceStackNavigator.ios.tsx b/apps/mobile/src/features/layout/createWorkspaceStackNavigator.ios.tsx index e642754a013b..7a3c9de54831 100644 --- a/apps/mobile/src/features/layout/createWorkspaceStackNavigator.ios.tsx +++ b/apps/mobile/src/features/layout/createWorkspaceStackNavigator.ios.tsx @@ -20,7 +20,7 @@ import { type NativeStackTypeBag, } from "@react-navigation/native-stack"; import { use, useCallback, useEffect, useMemo, useRef, type ComponentProps } from "react"; -import { View } from "react-native"; +import { Platform, View } from "react-native"; import { Split, type SplitHostCommands } from "react-native-screens"; import { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { scopedThreadKey } from "../../lib/scopedEntities"; @@ -33,8 +33,13 @@ import { V5CardStackView, V5StackView, } from "../../native/createV5StackNavigator.ios"; -import { NATIVE_WORKSPACE_COLUMNS_SUPPORTED } from "../../native/NativeWorkspaceColumns"; +import { + useNativeWorkspaceColumnsReady, + useNativeWorkspaceColumnsSupported, +} from "../../native/NativeWorkspaceColumns"; import { V5StackHeader } from "../../native/V5StackHeader.ios"; +import type { AppNativeStackNavigationOptions } from "../../native/StackHeader"; +import { dispatchHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; import { nativeWorkspacePopAction, projectWorkspaceStack, @@ -89,7 +94,12 @@ function ColumnScreen(props: { canGoBack={!props.primary} primary={props.primary} /> - + {descriptor.render()} @@ -107,6 +117,8 @@ function WorkspaceColumns( const inspector = use(NativeWorkspaceInspectorContext); const hostRef = useRef(null); const activeDetailKey = props.detail.at(-1)?.key; + const compactColumn = activeDetailKey ? "secondary" : "primary"; + const shownCompactColumn = useRef(compactColumn); const threadParams = props.detail.findLast((route) => route.name === "Thread")?.params; const selectedThreadKey = threadParams && @@ -131,9 +143,12 @@ function WorkspaceColumns( useEffect(() => { // In compact size classes UIKit exposes one column. Selecting a thread // changes the visible column without rebuilding either navigation stack. - if (layout.usesSplitView) return; - hostRef.current?.show(activeDetailKey ? "secondary" : "primary"); - }, [activeDetailKey, layout.usesSplitView]); + // The initial column is a native prop, so it is applied before the host + // attaches. Commands only handle later navigation changes. + if (layout.usesSplitView || shownCompactColumn.current === compactColumn) return; + shownCompactColumn.current = compactColumn; + hostRef.current?.show(compactColumn); + }, [compactColumn, layout.usesSplitView]); const primary = props.descriptors[props.primary.key]; if (!primary) return null; @@ -142,6 +157,7 @@ function WorkspaceColumns( ref={hostRef} testID="adaptive-workspace-layout" preferredSplitBehavior="tile" + topColumnForCollapsing={compactColumn} preferredDisplayMode={ panes.primarySidebarVisible || !activeDetailKey ? "oneBesideSecondary" : "secondaryOnly" } @@ -153,7 +169,7 @@ function WorkspaceColumns( }} columnMetrics={{ minimumPrimaryColumnWidth: 280, - maximumPrimaryColumnWidth: 380, + maximumPrimaryColumnWidth: Math.max(380, layout.listPaneWidth ?? 0), preferredPrimaryColumnWidthOrFraction: layout.listPaneWidth ?? 320, minimumSecondaryColumnWidth: 320, }} @@ -179,6 +195,37 @@ function WorkspaceColumns( options={{ headerShown: true, title: "", + unstable_headerRightItems: () => + Platform.OS === "ios" && !Platform.isPad && layout.usesSplitView + ? [ + ...(!panes.primarySidebarVisible + ? [ + { + type: "button" as const, + axisBehavior: "verticalPreferred" as const, + accessibilityLabel: "Search threads", + label: "Search threads", + icon: { + type: "sfSymbol" as const, + name: "magnifyingglass" as const, + }, + onPress: () => { + dispatchHardwareKeyboardCommand("focusSearch"); + }, + }, + ] + : []), + { + type: "button", + axisBehavior: "verticalPreferred", + accessibilityLabel: "New task", + label: "New task", + icon: { type: "sfSymbol", name: "square.and.pencil" }, + onPress: () => + props.navigation.navigate("NewTaskSheet", { screen: "NewTask" }), + }, + ] + : [], unstable_headerLeftItems: () => [ { type: "button", @@ -293,6 +340,8 @@ function WorkspaceStackNavigator({ UNSTABLE_router, ...rest }: NativeStackNavigatorProps) { + const usesNativeWorkspaceColumns = useNativeWorkspaceColumnsSupported(); + const nativeWorkspaceColumnsReady = useNativeWorkspaceColumnsReady(); const { state, describe, descriptors, navigation, NavigationContent } = useNavigationBuilder< StackNavigationState, StackRouterOptions, @@ -310,9 +359,10 @@ function WorkspaceStackNavigator({ screenLayout, UNSTABLE_router, }); - const WorkspaceView = NATIVE_WORKSPACE_COLUMNS_SUPPORTED ? WorkspaceStackView : V5StackView; + if (!nativeWorkspaceColumnsReady) return null; + const WorkspaceView = usesNativeWorkspaceColumns ? WorkspaceStackView : V5StackView; return ( - + [ withNativeGlassHeaderItem({ + axisBehavior: "horizontalOnly", accessibilityLabel: "Filter settings environments and projects", icon: { name: filterIcon, type: "sfSymbol" }, label: "", @@ -120,6 +121,7 @@ export function SettingsEnvironmentFilterHeader(props: { ...(closeSettings ? [ withNativeGlassHeaderItem({ + axisBehavior: "horizontalOnly", accessibilityLabel: "Close settings", icon: { name: "xmark", type: "sfSymbol" }, identifier: "settings-close", diff --git a/apps/mobile/src/features/threads/NewTaskContextPickerScreens.tsx b/apps/mobile/src/features/threads/NewTaskContextPickerScreens.tsx index 2f8fc3a33e7a..8330a6b1b54f 100644 --- a/apps/mobile/src/features/threads/NewTaskContextPickerScreens.tsx +++ b/apps/mobile/src/features/threads/NewTaskContextPickerScreens.tsx @@ -37,8 +37,8 @@ import { vcsEnvironment } from "../../state/vcs"; import { createNativeMailSearchToolbarItem, NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET, - NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED, } from "../layout/native-mail-search-toolbar"; +import { useNativeMailSearchToolbar } from "../../native/use-native-mail-search-toolbar"; import { branchBadgeLabel, useNewTaskFlow } from "./new-task-flow-provider"; import { checkoutNewTaskBranch } from "./checkout-new-task-branch"; @@ -413,7 +413,7 @@ export function BranchPickerScreen(props: { }) { const navigation = useNavigation(); const insets = useSafeAreaInsets(); - const usesNativeMailSearchToolbar = Platform.OS === "ios" && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED; + const usesNativeMailSearchToolbar = useNativeMailSearchToolbar(); const selectedBranchName = props.selectedBranchName ?? props.branches.find((branch) => branch.current)?.name ?? diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index b6930deb5e86..9229c117d8da 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -14,6 +14,8 @@ import { useKeyboardChatComposerInset, useKeyboardScrollToEnd } from "@legendapp import { resolveProviderSkillsForCwd } from "@t3tools/client-runtime/providerSkills"; import type { LegendListRef } from "@legendapp/list/react-native"; import { HeaderHeightContext } from "@react-navigation/elements"; +import { NativeLayoutObserver } from "../../native/NativeLayoutObserver"; +import { deriveBottomControlInsets, type NativeLayoutMetrics } from "../../lib/reserved-regions"; import type { EnvironmentId, MessageId, @@ -84,6 +86,8 @@ import Animated, { } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useWorkspaceContentWidth } from "../layout/workspace-content-width"; +import { useNativeWorkspaceColumnsSupported } from "../../native/NativeWorkspaceColumns"; +import { useNativeColumnLayoutMetrics } from "../../native/native-layout-metrics"; import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; import { collectProviderUsageLimits } from "@t3tools/shared/usageLimits"; import type { ComposerEditorHandle } from "../../components/ComposerEditor"; @@ -317,6 +321,7 @@ const USER_INPUT_TOGGLE_TIMING = { }; export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: ThreadDetailScreenProps) { + const usesNativeWorkspaceColumns = useNativeWorkspaceColumnsSupported(); const navigation = useNavigation(); const { session: voiceInputSession } = useGlobalVoiceInput(); const reportedModelSelection = useThreadReportedModelSelection({ @@ -388,7 +393,11 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread } }, []); const windowHeight = useWindowDimensions().height; - const navigationHeaderHeight = useContext(HeaderHeightContext) || insets.top + 44; + const navigationHeaderHeight = useContext(HeaderHeightContext) ?? insets.top + 44; + const [screenMetrics, setScreenMetrics] = useState(null); + const columnMetrics = useNativeColumnLayoutMetrics(); + const nativeMetrics = usesNativeWorkspaceColumns ? columnMetrics : screenMetrics; + const controlInsets = deriveBottomControlInsets(nativeMetrics); const agentLabel = `${props.selectedThread.modelSelection.instanceId} agent`; const selectedThreadKey = scopedThreadKey(props.environmentId, props.selectedThread.id); const composerError = useAtomValue(threadComposerErrorsAtom)[selectedThreadKey]?.message ?? null; @@ -826,8 +835,14 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread // Assign both layouts explicitly so the dock always follows its current parent. const composerWidthStyle = useAnimatedStyle(() => isSplitLayout && workspaceContentWidth !== null - ? { width: workspaceContentWidth.value } - : { width: "100%" }, + ? { + width: Math.max( + 0, + workspaceContentWidth.value - controlInsets.left - controlInsets.right, + ), + right: undefined, + } + : { width: undefined, right: controlInsets.right }, ); const selectedInstanceId = props.selectedThread.modelSelection.instanceId; useStreamingHaptics(props.selectedThread.id, props.selectedThreadFeed); @@ -1083,6 +1098,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread return ( + {!usesNativeWorkspaceColumns ? : null} {showContent ? ( {/* No paddingTop here: the overlay's measured height becomes the list's bottom inset, so any padding above the pill/composer diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 44673dcf2759..a210ffcb1dc6 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -57,6 +57,7 @@ import { videoMimeType } from "@t3tools/shared/video"; import { SymbolView, type AppSymbolName } from "../../components/AppSymbol"; import { HeaderHeightContext } from "@react-navigation/elements"; import { useFocusEffect, useNavigation } from "@react-navigation/native"; +import { useNativeWorkspaceColumnsSupported } from "../../native/NativeWorkspaceColumns"; import { createContext, memo, @@ -297,6 +298,7 @@ export interface ThreadFeedProps { readonly contentBottomInset?: number; readonly historyControls?: ThreadFeedHistoryControls; readonly contentMaxWidth?: number; + readonly contentSideInsets?: { readonly left: number; readonly right: number }; readonly layoutVariant?: LayoutVariant; readonly usesAutomaticContentInsets?: boolean; readonly onHeaderMaterialVisibilityChange?: (visible: boolean) => void; @@ -1543,6 +1545,7 @@ function renderFeedEntry( readonly reviewCommentColors: ReviewCommentColors; readonly reviewCommentBubbleWidth: number; readonly themeAppearance: "light" | "dark"; + readonly usesNativeWorkspaceColumns: boolean; readonly userBubbleMaxWidth: number; /** Width assistant markdown lays out in, so images can size their frame before layout. */ readonly markdownContentWidth: number; @@ -1730,11 +1733,19 @@ function renderFeedEntry( className="min-w-0 gap-2 rounded-[20px] px-3.5 py-2.5" style={{ backgroundColor: userBubbleColor, - maxWidth: props.userBubbleMaxWidth, + maxWidth: + props.usesNativeWorkspaceColumns && Platform.OS === "ios" && !Platform.isPad + ? "85%" + : props.userBubbleMaxWidth, ...(hasReviewCommentContext ? { width: props.reviewCommentBubbleWidth } : hasWideBlock - ? { width: props.userBubbleMaxWidth } + ? { + width: + props.usesNativeWorkspaceColumns && Platform.OS === "ios" && !Platform.isPad + ? "85%" + : props.userBubbleMaxWidth, + } : null), }} > @@ -2174,6 +2185,7 @@ function ThreadFeedPlaceholder(props: { } export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { + const usesNativeWorkspaceColumns = useNativeWorkspaceColumnsSupported(); const navigation = useNavigation(); const { themeAppearance } = useAppearancePreferences(); const copyFeedbackTimeoutRef = useRef | null>(null); @@ -2257,12 +2269,15 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { setExpandedFile(null); }, [props.environmentId, props.threadId, props.contentPresentation.kind]); const horizontalPadding = props.layoutVariant === "split" ? 20 : 16; + const contentLeftInset = props.contentSideInsets?.left ?? 0; + const contentRightInset = props.contentSideInsets?.right ?? 0; + const usableViewportWidth = Math.max(0, viewportWidth - contentLeftInset - contentRightInset); const contentHorizontalPadding = deriveCenteredContentHorizontalPadding({ - viewportWidth, + viewportWidth: usableViewportWidth, maxContentWidth: props.contentMaxWidth ?? null, minimumPadding: horizontalPadding, }); - const contentWidth = Math.max(0, viewportWidth - contentHorizontalPadding * 2); + const contentWidth = Math.max(0, usableViewportWidth - contentHorizontalPadding * 2); const userBubbleMaxWidth = contentWidth * 0.85; const markdownContentWidth = Math.max(0, contentWidth - ASSISTANT_ROW_HORIZONTAL_PADDING * 2); const reviewCommentBubbleWidth = Math.min(Math.max(280, contentWidth * 0.85), contentWidth); @@ -2285,7 +2300,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { // header-providing screen) and fall back to the standard iOS bar height. const navigationHeaderHeight = useContext(HeaderHeightContext); const anchorTopInset = usesNativeAutomaticInsets - ? navigationHeaderHeight || insets.top + IOS_NAV_BAR_HEIGHT + ? (navigationHeaderHeight ?? insets.top + IOS_NAV_BAR_HEIGHT) : topContentInset; const theme = useUniwindTheme(); @@ -3030,6 +3045,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { reviewCommentColors, reviewCommentBubbleWidth, themeAppearance, + usesNativeWorkspaceColumns, userBubbleMaxWidth, markdownContentWidth, contentWidth, @@ -3067,6 +3083,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { reviewCommentColors, reviewCommentBubbleWidth, themeAppearance, + usesNativeWorkspaceColumns, userBubbleMaxWidth, markdownContentWidth, contentWidth, @@ -3119,7 +3136,18 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { // (facebook/react-native#54123); the anchored end space after a send // is pure inset, so without this the blank region can't be scrolled. applyWorkaroundForContentInsetHitTestBug - contentInsetAdjustmentBehavior={usesNativeAutomaticInsets ? "automatic" : "never"} + // Horizontal Duo reservations are already included in the row padding. + // Let UIKit adjust the scrolling axis without shifting content sideways. + contentInsetAdjustmentBehavior={ + usesNativeAutomaticInsets + ? usesNativeWorkspaceColumns && + Platform.OS === "ios" && + !Platform.isPad && + props.layoutVariant === "split" + ? "scrollableAxes" + : "automatic" + : "never" + } automaticallyAdjustsScrollIndicatorInsets={usesNativeAutomaticInsets} {...(usesNativeAutomaticInsets ? { @@ -3231,7 +3259,8 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { } contentContainerStyle={{ paddingTop: 12, - paddingHorizontal: contentHorizontalPadding, + paddingLeft: contentHorizontalPadding + contentLeftInset, + paddingRight: contentHorizontalPadding + contentRightInset, }} /> @@ -3239,7 +3268,10 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { !props.worktreeSetup && props.activeWorkStartedAt === null && props.contentPresentation.kind === "ready" ? ( - + provider.instanceId === session.providerInstanceId) ?? null) : null; - const bottomToolbarInset = - Platform.OS === "ios" && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED - ? NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET - : 0; + const usesNativeMailSearchToolbar = useNativeMailSearchToolbar(); + const bottomToolbarInset = usesNativeMailSearchToolbar + ? NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET + : 0; return ( @@ -1107,7 +1107,7 @@ function ThreadSettingsModelsScreen() { const session = useThreadSettingsSession(); const presentation = useThreadSettingsPickerPresentation(); const navigation = useNavigation>(); - const usesNativeMailSearchToolbar = Platform.OS === "ios" && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED; + const usesNativeMailSearchToolbar = useNativeMailSearchToolbar(); const hasCustomCatalogFilter = session.providerFilter !== null || session.showLegacy; const commitAndClose = useCallback(() => { if (!session.commitPendingModel()) return; diff --git a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx index 1bcb225bf810..1741c527d91d 100644 --- a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx @@ -21,6 +21,7 @@ import { Alert, Platform, Pressable, RefreshControl, ScrollView, View } from "re import { Screen, ScreenStack, ScreenStackHeaderConfig } from "react-native-screens"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { NativeSheetContent } from "../../../native/NativeSheetContent"; import { useUniwindTheme } from "../../../lib/useUniwindTheme"; import { AndroidHeaderIconButton, @@ -411,7 +412,7 @@ function GitOverviewSheetContent(props: GitOverviewSheetProps) { scrollEdgeEffects={HEADER_SCROLL_EDGE_EFFECTS} style={{ backgroundColor: sheetColor, flex: 1 }} > - {content} + {content} - {content} + {content} [0]; readonly onReturnToThread?: () => void; }) { + const usesNativeWorkspaceColumns = useNativeWorkspaceColumnsSupported(); + const usesDuoHeader = usesNativeWorkspaceColumns && Platform.OS === "ios" && !Platform.isPad; const navigation = useNavigation(); const { layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); const threadCenterHeaderItems = useThreadGitCenterHeaderItems(props.gitControls); const compactRightHeaderItems = useThreadGitRightHeaderItems(props.gitControls); const splitLeftHeaderItems = useMemo( () => [ - { - // Match Mail's split-view detail toolbar: the first detail action sits - // inside the content pane, not flush against the sidebar divider. - spacing: 18, - type: "spacing" as const, - }, + ...(!usesDuoHeader + ? [ + { + // Match Mail's split-view detail toolbar: the first detail action sits + // inside the content pane, not flush against the sidebar divider. + spacing: 18, + type: "spacing" as const, + }, + ] + : []), ...(props.onReturnToThread ? [ withNativeGlassHeaderItem({ @@ -43,6 +52,7 @@ export function useThreadHeaderOptions(props: { ] : []), withNativeGlassHeaderItem({ + axisBehavior: usesNativeWorkspaceColumns ? "horizontalOnly" : undefined, accessibilityLabel: panes.primarySidebarVisible ? "Maximize content" : "Show thread sidebar", @@ -51,18 +61,31 @@ export function useThreadHeaderOptions(props: { type: "sfSymbol" as const, }, identifier: "thread-left-sidebar", + label: panes.primarySidebarVisible ? "Maximize content" : "Show thread sidebar", onPress: togglePrimarySidebar, type: "button" as const, }), - withNativeGlassHeaderItem({ - accessibilityLabel: "New task", - icon: { name: "square.and.pencil", type: "sfSymbol" as const }, - identifier: "thread-left-new-task", - onPress: () => navigation.navigate("NewTaskSheet", { screen: "NewTask" }), - type: "button" as const, - }), + ...(!usesDuoHeader + ? [ + withNativeGlassHeaderItem({ + accessibilityLabel: "New task", + icon: { name: "square.and.pencil", type: "sfSymbol" as const }, + identifier: "thread-left-new-task", + label: "New task", + onPress: () => navigation.navigate("NewTaskSheet", { screen: "NewTask" }), + type: "button" as const, + }), + ] + : []), + ], + [ + panes.primarySidebarVisible, + props.onReturnToThread, + navigation, + togglePrimarySidebar, + usesDuoHeader, + usesNativeWorkspaceColumns, ], - [panes.primarySidebarVisible, props.onReturnToThread, navigation, togglePrimarySidebar], ); // Deep links / cold starts land with Thread as the ONLY route, where the // native back button does not render. Provide an explicit Home escape for @@ -81,6 +104,41 @@ export function useThreadHeaderOptions(props: { [navigation], ); + const duoRightHeaderItems = useMemo( + () => [ + ...threadCenterHeaderItems, + { type: "spacing", spacing: 8 }, + ...(layout.usesSplitView && + Platform.OS === "ios" && + !Platform.isPad && + !panes.primarySidebarVisible + ? [ + withNativeGlassHeaderItem({ + type: "button" as const, + axisBehavior: "verticalPreferred", + identifier: "thread-right-search", + label: "Search threads", + accessibilityLabel: "Search threads", + icon: { name: "magnifyingglass", type: "sfSymbol" as const }, + onPress: () => { + dispatchHardwareKeyboardCommand("focusSearch"); + }, + }), + ] + : []), + withNativeGlassHeaderItem({ + type: "button" as const, + axisBehavior: "verticalPreferred", + identifier: "thread-right-new-task", + label: "New task", + accessibilityLabel: "New task", + icon: { name: "square.and.pencil", type: "sfSymbol" as const }, + onPress: () => navigation.navigate("NewTaskSheet", { screen: "NewTask" }), + }), + ], + [navigation, threadCenterHeaderItems, layout.usesSplitView, panes.primarySidebarVisible], + ); + const options: AppNativeStackNavigationOptions = { headerShown: true, headerTitle: props.title, @@ -104,7 +162,12 @@ export function useThreadHeaderOptions(props: { // the git controls on the RIGHT (no center items — center space is // reserved for future breadcrumbs/status). unstable_headerRightItems: () => - layout.usesSplitView ? threadCenterHeaderItems : compactRightHeaderItems, + usesDuoHeader + ? duoRightHeaderItems + : layout.usesSplitView + ? threadCenterHeaderItems + : compactRightHeaderItems, + unstable_headerToolbarItems: () => [], unstable_headerSubtitle: props.usesNativeHeaderGlass ? props.subtitle : undefined, contentStyle: undefined, }; @@ -116,7 +179,11 @@ export function useThreadHeaderOptions(props: { // callbacks also read state the items do not display (a "Push" item runs `push` or // `commit_push` depending on the default ref), so that state is keyed too. optionsVersion: [ - layout.usesSplitView ? threadCenterHeaderItems : compactRightHeaderItems, + splitLeftHeaderItems, + threadCenterHeaderItems, + compactRightHeaderItems, + duoRightHeaderItems, + usesDuoHeader, environmentId, threadId, gitStatus?.isDefaultRef, diff --git a/apps/mobile/src/lib/layout.ts b/apps/mobile/src/lib/layout.ts index ee38ac020e74..ac4cde7789b0 100644 --- a/apps/mobile/src/lib/layout.ts +++ b/apps/mobile/src/lib/layout.ts @@ -1,5 +1,6 @@ import { scaledTypographyLineHeight } from "./appearancePreferences"; import { MOBILE_TYPOGRAPHY } from "./typography"; +import type { NativeLayoutMetrics } from "./reserved-regions"; function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); @@ -56,6 +57,7 @@ export interface Layout { readonly variant: LayoutVariant; readonly usesSplitView: boolean; readonly listPaneWidth: number | null; + readonly listPaneGap?: number; readonly shellPadding: number; } @@ -87,9 +89,25 @@ export function deriveThreadFeedInitialContentInset(input: { export type WorkspaceAuxiliaryPaneRole = "supplementary" | "inspector"; -export function deriveLayout(input: { readonly width: number; readonly height: number }): Layout { +export function deriveLayout(input: { + readonly width: number; + readonly height: number; + readonly nativeMetrics?: NativeLayoutMetrics | null; +}): Layout { const { width, height } = input; - const wideEnoughForSplit = width >= SPLIT_LAYOUT_MIN_WIDTH && height >= SPLIT_LAYOUT_MIN_HEIGHT; + const metrics = input.nativeMetrics; + const usableWidth = width - (metrics?.safeArea.left ?? 0) - (metrics?.safeArea.right ?? 0); + // UIKit's size class supports the shorter Duo inner display. Geometry remains + // a floor so a narrow multitasking window cannot squeeze both columns. + const hasDuoGeometry = + metrics && + (metrics.verticalBarEdge !== "none" || + metrics.reservedRegions.some((region) => region.kind === "division")); + const wideEnoughForSplit = hasDuoGeometry + ? metrics.horizontalSizeClass === "regular" && + usableWidth >= SPLIT_SIDEBAR_MIN_WIDTH + 320 && + height >= 400 + : width >= SPLIT_LAYOUT_MIN_WIDTH && height >= SPLIT_LAYOUT_MIN_HEIGHT; if (!wideEnoughForSplit) { return { @@ -100,14 +118,20 @@ export function deriveLayout(input: { readonly width: number; readonly height: n }; } + const division = metrics?.reservedRegions.find( + (region) => + region.kind === "division" && + region.height >= height / 2 && + region.x - metrics.safeArea.left >= SPLIT_SIDEBAR_MIN_WIDTH && + width - region.x - region.width - metrics.safeArea.right >= 320, + ); return { variant: "split", usesSplitView: true, - listPaneWidth: clamp( - Math.round(width * 0.32), - SPLIT_SIDEBAR_MIN_WIDTH, - SPLIT_SIDEBAR_DEFAULT_MAX_WIDTH, - ), + listPaneWidth: + division?.x ?? + clamp(Math.round(width * 0.32), SPLIT_SIDEBAR_MIN_WIDTH, SPLIT_SIDEBAR_DEFAULT_MAX_WIDTH), + ...(division ? { listPaneGap: division.width } : {}), shellPadding: 0, }; } @@ -125,7 +149,7 @@ export function deriveWorkspacePaneLayout(input: { const preferredPrimarySidebarVisible = input.layout.usesSplitView && input.primarySidebarPreferredVisible; const preferredPrimarySidebarWidth = preferredPrimarySidebarVisible - ? (input.layout.listPaneWidth ?? 0) + ? (input.layout.listPaneWidth ?? 0) + (input.layout.listPaneGap ?? 0) : 0; if (auxiliaryPaneRole === "inspector") { @@ -141,11 +165,14 @@ export function deriveWorkspacePaneLayout(input: { auxiliaryPaneVisible && fileInspector.width !== null && input.layout.listPaneWidth !== null && - viewportWidth - input.layout.listPaneWidth - fileInspector.width < + viewportWidth - + input.layout.listPaneWidth - + (input.layout.listPaneGap ?? 0) - + fileInspector.width < FILE_INSPECTOR_MIN_MAIN_WIDTH; const primarySidebarVisible = preferredPrimarySidebarVisible && !primarySidebarSuppressedByAuxiliary; - const primarySidebarWidth = primarySidebarVisible ? (input.layout.listPaneWidth ?? 0) : 0; + const primarySidebarWidth = primarySidebarVisible ? preferredPrimarySidebarWidth : 0; return { primarySidebarVisible, diff --git a/apps/mobile/src/lib/reserved-regions.test.ts b/apps/mobile/src/lib/reserved-regions.test.ts new file mode 100644 index 000000000000..e742169bd94a --- /dev/null +++ b/apps/mobile/src/lib/reserved-regions.test.ts @@ -0,0 +1,140 @@ +import { describe, expect, it } from "vite-plus/test"; +import { deriveBottomControlInsets, type NativeLayoutMetrics } from "./reserved-regions"; +import { deriveLayout, deriveWorkspacePaneLayout } from "./layout"; + +const innerDisplay: NativeLayoutMetrics = { + width: 960, + height: 560, + horizontalSizeClass: "regular", + verticalBarEdge: "right", + safeArea: { top: 0, bottom: 20, left: 8, right: 72 }, + reservedRegions: [], +}; + +describe("bottom controls around reserved regions", () => { + it("respects asymmetric side controls without reserving the bottom twice", () => { + expect(deriveBottomControlInsets(innerDisplay)).toEqual({ left: 8, right: 72, bottom: 0 }); + }); + + it("leaves ordinary non-native clients unchanged", () => { + expect(deriveBottomControlInsets(null)).toEqual({ left: 0, right: 0, bottom: 0 }); + }); + + it("keeps the editor entirely on one side of a book fold", () => { + const insets = deriveBottomControlInsets({ + ...innerDisplay, + reservedRegions: [{ kind: "division", x: 456, y: 0, width: 48, height: 560 }], + }); + expect(insets).toEqual({ left: 8, right: 504, bottom: 0 }); + expect(innerDisplay.width - insets.left - insets.right).toBeGreaterThan(320); + }); + + it("keeps controls below a horizontal fold in a hands-free pose", () => { + expect( + deriveBottomControlInsets({ + ...innerDisplay, + reservedRegions: [{ kind: "division", x: 0, y: 240, width: 960, height: 40 }], + }), + ).toEqual({ left: 8, right: 72, bottom: 0 }); + }); + + it("moves a control away from a bottom camera while leaving a top camera alone", () => { + expect( + deriveBottomControlInsets({ + ...innerDisplay, + reservedRegions: [ + { kind: "occlusion", x: 8, y: 0, width: 60, height: 60 }, + { kind: "occlusion", x: 8, y: 510, width: 60, height: 50 }, + ], + }), + ).toEqual({ left: 68, right: 72, bottom: 0 }); + }); + + it("restores the full editor width when the device opens flat", () => { + const folded = { + ...innerDisplay, + reservedRegions: [{ kind: "division" as const, x: 456, y: 0, width: 48, height: 560 }], + }; + expect(deriveBottomControlInsets(folded)).not.toEqual(deriveBottomControlInsets(innerDisplay)); + expect(deriveBottomControlInsets(innerDisplay)).toEqual({ left: 8, right: 72, bottom: 0 }); + }); +}); + +describe("Duo workspace sizing", () => { + it("shows both columns on a short regular-width inner display", () => { + const layout = deriveLayout({ ...innerDisplay, nativeMetrics: innerDisplay }); + expect(layout.usesSplitView).toBe(true); + expect( + deriveWorkspacePaneLayout({ + layout, + viewportWidth: innerDisplay.width, + primarySidebarPreferredVisible: true, + auxiliaryPanePreferredVisible: false, + }).contentPaneWidth, + ).toBeGreaterThan(320); + }); + + it("keeps an ordinary phone compact when it rotates", () => { + expect(deriveLayout({ width: 852, height: 393 }).usesSplitView).toBe(false); + for (const [width, height] of [ + [932, 430], + [650, 500], + ]) { + const metrics: NativeLayoutMetrics = { + ...innerDisplay, + width: width!, + height: height!, + verticalBarEdge: "none", + safeArea: { top: 0, bottom: 20, left: 59, right: 59 }, + }; + expect(deriveLayout({ ...metrics, nativeMetrics: metrics }).usesSplitView).toBe(false); + const cameraMetrics = { + ...metrics, + reservedRegions: [{ kind: "occlusion" as const, x: 0, y: 0, width: 60, height: 60 }], + }; + expect(deriveLayout({ ...cameraMetrics, nativeMetrics: cameraMetrics }).usesSplitView).toBe( + false, + ); + } + expect( + deriveLayout({ + width: 960, + height: 560, + nativeMetrics: { + ...innerDisplay, + horizontalSizeClass: "compact", + }, + }).usesSplitView, + ).toBe(false); + }); + + it("requires enough safe width for both panes in a multitasking window", () => { + const metrics = { ...innerDisplay, width: 660 }; + expect(deriveLayout({ ...metrics, nativeMetrics: metrics }).usesSplitView).toBe(false); + }); + + it("aligns the panes to the book fold and accounts for the reserved gap", () => { + const metrics = { + ...innerDisplay, + reservedRegions: [{ kind: "division" as const, x: 456, y: 0, width: 48, height: 560 }], + }; + const layout = deriveLayout({ ...metrics, nativeMetrics: metrics }); + expect(layout.listPaneWidth).toBe(456); + expect(layout.listPaneGap).toBe(48); + const panes = deriveWorkspacePaneLayout({ + layout, + viewportWidth: 960, + primarySidebarPreferredVisible: true, + auxiliaryPanePreferredVisible: false, + }); + expect(panes.contentPaneWidth).toBe(456); + expect( + deriveWorkspacePaneLayout({ + layout, + viewportWidth: 960, + primarySidebarPreferredVisible: false, + auxiliaryPanePreferredVisible: false, + }).contentPaneWidth, + ).toBe(960); + }); +}); diff --git a/apps/mobile/src/lib/reserved-regions.ts b/apps/mobile/src/lib/reserved-regions.ts index 318896239536..38496931f9de 100644 --- a/apps/mobile/src/lib/reserved-regions.ts +++ b/apps/mobile/src/lib/reserved-regions.ts @@ -1,12 +1,51 @@ -/** Geometry of the body inside a native navigation column. */ +export interface ReservedRegion { + readonly kind: "division" | "occlusion"; + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; +} + export interface NativeLayoutMetrics { readonly width: number; readonly height: number; + readonly hasHinge?: boolean; readonly horizontalSizeClass: "compact" | "regular"; + readonly verticalBarEdge: "none" | "left" | "right"; readonly safeArea: { readonly top: number; readonly bottom: number; readonly left: number; readonly right: number; }; + readonly reservedRegions: ReadonlyArray; +} + +/** + * Keep bottom controls in one usable region. A book-like fold selects the + * larger side; a horizontal fold leaves the controls on the lower display. + * Text and other scrollable content can continue to use the full canvas. + */ +export function deriveBottomControlInsets(metrics: NativeLayoutMetrics | null) { + if (!metrics) return { left: 0, right: 0, bottom: 0 }; + let left = metrics.safeArea.left; + let right = metrics.width - metrics.safeArea.right; + let bottom = metrics.height; + for (const region of metrics.reservedRegions) { + const x = Math.max(left, region.x); + const endX = Math.min(right, region.x + region.width); + const y = Math.max(0, region.y); + const endY = Math.min(bottom, region.y + region.height); + if (endX <= x || endY <= y) continue; + if (region.kind === "division" && region.height >= metrics.height / 2) { + if (right - endX >= x - left) left = endX; + else right = x; + } else if (endY >= bottom - metrics.safeArea.bottom) { + // An occlusion at the bottom can also occupy just one corner. + if (region.width >= right - left) bottom = y; + else if (right - endX >= x - left) left = endX; + else right = x; + } + } + return { left, right: metrics.width - right, bottom: metrics.height - bottom }; } diff --git a/apps/mobile/src/native/NativeColumnContent.ios.tsx b/apps/mobile/src/native/NativeColumnContent.ios.tsx index 77b03098b36c..8a8fa0f599c8 100644 --- a/apps/mobile/src/native/NativeColumnContent.ios.tsx +++ b/apps/mobile/src/native/NativeColumnContent.ios.tsx @@ -9,12 +9,16 @@ import { NativeLayoutObserver } from "./NativeLayoutObserver"; export function NativeColumnContent(props: { readonly children: ReactNode; readonly primary?: boolean; + readonly insetHorizontally?: boolean; }) { const [metrics, setMetrics] = useState(null); return ( - + {/* Observe the inset body. UIKit can reserve other columns in its safe area. */} diff --git a/apps/mobile/src/native/NativeHeaderToolbar.tsx b/apps/mobile/src/native/NativeHeaderToolbar.tsx index 5bf5aa27c91d..71a18fa5c2c6 100644 --- a/apps/mobile/src/native/NativeHeaderToolbar.tsx +++ b/apps/mobile/src/native/NativeHeaderToolbar.tsx @@ -14,6 +14,7 @@ import { type ReactNode, } from "react"; import type { ColorValue } from "react-native"; +import type { HeaderBarButtonSearchBarPlacementItem } from "react-native-screens"; function useNativeStackNavigation(): NativeStackNavigationProp | null { return useNavigation>(); @@ -34,8 +35,13 @@ function labelFromChildren(children: ReactNode): string { type NativeStackHeaderIcon = NonNullable< Extract["icon"] >; +type NativeStackHeaderAxisBehavior = Extract< + NativeStackHeaderItem, + { type: "button" } +>["axisBehavior"]; +type NativeToolbarItem = NativeStackHeaderItem | HeaderBarButtonSearchBarPlacementItem; type NativeStackOptionsWithToolbar = NativeStackNavigationOptions & { - unstable_headerToolbarItems?: () => NativeStackHeaderItem[]; + unstable_headerToolbarItems?: () => NativeToolbarItem[]; }; function iconFromProp(icon: unknown): NativeStackHeaderIcon | undefined { @@ -112,7 +118,7 @@ function collectMenuItems(children: ReactNode): NativeStackHeaderItemMenu["menu" return items; } -function convertToolbarChild(child: ReactNode): NativeStackHeaderItem | null { +function convertToolbarChild(child: ReactNode): NativeToolbarItem | null { if (!isValidElement(child)) { return null; } @@ -121,15 +127,22 @@ function convertToolbarChild(child: ReactNode): NativeStackHeaderItem | null { if (typeName === "NativeHeaderToolbarCustom" && isValidElement(child.props.children)) { return { type: "custom", element: child.props.children }; } + if (typeName === "NativeHeaderToolbarSearchBarSlot") { + return { type: "searchBarPlacement" }; + } if (typeName === "NativeHeaderToolbarButton") { return { type: "button", - label: typeof child.props.label === "string" ? child.props.label : "", + label: + typeof child.props.label === "string" + ? child.props.label + : String(child.props.accessibilityLabel ?? ""), accessibilityLabel: typeof child.props.accessibilityLabel === "string" ? child.props.accessibilityLabel : undefined, + axisBehavior: child.props.axisBehavior as NativeStackHeaderAxisBehavior, disabled: Boolean(child.props.disabled), icon: iconFromProp(child.props.icon), onPress: @@ -173,8 +186,8 @@ function convertToolbarChild(child: ReactNode): NativeStackHeaderItem | null { return null; } -function collectToolbarItems(children: ReactNode): NativeStackHeaderItem[] { - const items: NativeStackHeaderItem[] = []; +function collectToolbarItems(children: ReactNode): NativeToolbarItem[] { + const items: NativeToolbarItem[] = []; Children.forEach(children, (child) => { const item = convertToolbarChild(child); if (item) { @@ -211,13 +224,17 @@ function NativeHeaderToolbarRoot(props: { } as NativeStackOptionsWithToolbar); }; } + // UIKit's search placement item belongs to the toolbar, never a header group. + const headerItems = items.filter( + (item): item is NativeStackHeaderItem => item.type !== "searchBarPlacement", + ); if (props.placement === "left") { - navigation.setOptions({ unstable_headerLeftItems: () => items }); + navigation.setOptions({ unstable_headerLeftItems: () => headerItems }); return () => { navigation.setOptions({ unstable_headerLeftItems: () => [] }); }; } - navigation.setOptions({ unstable_headerRightItems: () => items }); + navigation.setOptions({ unstable_headerRightItems: () => headerItems }); return () => { navigation.setOptions({ unstable_headerRightItems: () => [] }); }; @@ -228,6 +245,7 @@ function NativeHeaderToolbarRoot(props: { function NativeHeaderToolbarButton(_props: { readonly accessibilityLabel?: string; + readonly axisBehavior?: NativeStackHeaderAxisBehavior; readonly disabled?: boolean; readonly icon?: string; readonly label?: string; diff --git a/apps/mobile/src/native/NativeSheetContent.tsx b/apps/mobile/src/native/NativeSheetContent.tsx new file mode 100644 index 000000000000..7b508f3bc944 --- /dev/null +++ b/apps/mobile/src/native/NativeSheetContent.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; +import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context"; + +import { useNativeWorkspaceColumnsSupported } from "./NativeWorkspaceColumns"; + +/** Reserve the sheet's own side bars without moving its native navigation chrome. */ +export function NativeSheetContent(props: { readonly children: ReactNode }) { + const usesNativeWorkspaceColumns = useNativeWorkspaceColumnsSupported(); + if (!usesNativeWorkspaceColumns) return <>{props.children}; + + // A floating sheet can have different insets from the workspace underneath it. + return ( + + + {props.children} + + + ); +} diff --git a/apps/mobile/src/native/NativeWorkspaceColumns.ios.tsx b/apps/mobile/src/native/NativeWorkspaceColumns.ios.tsx index c85529b1dbf2..9aee432ed28d 100644 --- a/apps/mobile/src/native/NativeWorkspaceColumns.ios.tsx +++ b/apps/mobile/src/native/NativeWorkspaceColumns.ios.tsx @@ -1,7 +1,29 @@ import { requireOptionalNativeModule } from "expo"; import { Platform } from "react-native"; -const nativeControls = requireOptionalNativeModule<{ readonly supportsWorkspaceColumns?: boolean }>( - "T3NativeControls", -); +import { useNativeLayoutMetrics } from "./native-layout-metrics"; + +const nativeControls = requireOptionalNativeModule<{ + readonly supportsWorkspaceColumns?: boolean; + readonly ViewPrototypes?: { readonly T3NativeControls_LayoutMetrics?: unknown }; +}>("T3NativeControls"); export const NATIVE_WORKSPACE_COLUMNS_SUPPORTED = Platform.OS === "ios" && Platform.isPad && nativeControls?.supportsWorkspaceColumns === true; + +export function useNativeWorkspaceColumnsReady() { + const metrics = useNativeLayoutMetrics(); + // Choose the phone's native host before attaching any screens. Older clients + // without the observer can immediately use the compact navigator. + return ( + (Platform.OS === "ios" && Platform.isPad) || + !nativeControls?.ViewPrototypes?.T3NativeControls_LayoutMetrics || + metrics !== null + ); +} + +export function useNativeWorkspaceColumnsSupported() { + const metrics = useNativeLayoutMetrics(); + return ( + NATIVE_WORKSPACE_COLUMNS_SUPPORTED || + (nativeControls?.supportsWorkspaceColumns === true && metrics?.hasHinge === true) + ); +} diff --git a/apps/mobile/src/native/NativeWorkspaceColumns.tsx b/apps/mobile/src/native/NativeWorkspaceColumns.tsx index 4bb7aa16ac9b..d8fd7b664c17 100644 --- a/apps/mobile/src/native/NativeWorkspaceColumns.tsx +++ b/apps/mobile/src/native/NativeWorkspaceColumns.tsx @@ -1,3 +1,11 @@ import { Platform } from "react-native"; // Android retains its existing navigator. The iOS file checks native availability. export const NATIVE_WORKSPACE_COLUMNS_SUPPORTED = Platform.OS === "ios" && Platform.isPad; + +export function useNativeWorkspaceColumnsReady() { + return true; +} + +export function useNativeWorkspaceColumnsSupported() { + return NATIVE_WORKSPACE_COLUMNS_SUPPORTED; +} diff --git a/apps/mobile/src/native/StackHeader.tsx b/apps/mobile/src/native/StackHeader.tsx index 4f8eadcc5a11..6857e244f71f 100644 --- a/apps/mobile/src/native/StackHeader.tsx +++ b/apps/mobile/src/native/StackHeader.tsx @@ -19,6 +19,8 @@ export type AppNativeStackNavigationOptions = Omit< NativeStackNavigationOptions, "headerTintColor" | "unstable_headerLeftItems" | "unstable_headerRightItems" > & { + /** Nested navigator leaves apply their own horizontal safe area. */ + readonly nativeContentInsetHorizontally?: boolean; readonly headerTintColor?: string | ColorValue; readonly unstable_headerCenterItems?: unknown; readonly unstable_headerLeftItems?: unknown; diff --git a/apps/mobile/src/native/V5StackHeader.ios.tsx b/apps/mobile/src/native/V5StackHeader.ios.tsx index 2c843dab7cdc..e37647f4e76d 100644 --- a/apps/mobile/src/native/V5StackHeader.ios.tsx +++ b/apps/mobile/src/native/V5StackHeader.ios.tsx @@ -6,6 +6,7 @@ import type { import { isValidElement, useRef } from "react"; import { Platform, StyleSheet } from "react-native"; import { useMobileNavigationTheme } from "../lib/useMobileNavigationTheme"; +import { useNativeLayoutMetrics } from "../native/native-layout-metrics"; import { SearchBar, Stack, @@ -110,6 +111,7 @@ function convertItems(items: NativeStackHeaderItem[], prefix: string): HeaderIte id, identifier: item.identifier, title: item.label || item.accessibilityLabel || undefined, + axisBehavior: item.axisBehavior, icon: convertIcon(item.icon), menu: item.disabled ? undefined : convertMenu(item.menu, `${id}:menu`), hidesSharedBackground: item.hidesSharedBackground, @@ -120,6 +122,7 @@ function convertItems(items: NativeStackHeaderItem[], prefix: string): HeaderIte id, identifier: item.identifier, title: item.label || item.accessibilityLabel || undefined, + axisBehavior: item.axisBehavior, icon: convertIcon(item.icon), onPress: item.disabled ? undefined : item.onPress, hidesSharedBackground: item.hidesSharedBackground, @@ -136,6 +139,7 @@ export function V5StackHeader(props: { }) { const { options } = props; const theme = useMobileNavigationTheme(); + const nativeMetrics = useNativeLayoutMetrics(); const searchRef = useRef(null); const itemProps = { tintColor: options.headerTintColor, canGoBack: props.canGoBack }; const leading = convertItems(options.unstable_headerLeftItems?.(itemProps) ?? [], "leading"); @@ -162,6 +166,7 @@ export function V5StackHeader(props: { bottom.push({ type: "item", id: mailSearch.filterButtonId ?? "filter", + axisBehavior: bottomSearch ? "horizontalOnly" : undefined, icon: { type: "sfSymbol", name: mailSearch.filterSystemImageName ?? "line.3.horizontal.decrease", @@ -171,7 +176,12 @@ export function V5StackHeader(props: { if (bottomSearch) { if (bottom.length) bottom.push({ type: "spacer", id: "before-search", sizing: "fixed", width: 8 }); - bottom.push({ type: "item", id: "home-search", searchBarPlacement: true }); + bottom.push({ + type: "item", + id: "home-search", + axisBehavior: "horizontalOnly", + searchBarPlacement: true, + }); if (mailSearch.onComposePress) bottom.push({ type: "spacer", id: "after-search", sizing: "fixed", width: 8 }); } @@ -179,10 +189,22 @@ export function V5StackHeader(props: { bottom.push({ type: "item", id: mailSearch.composeButtonId ?? "compose", + axisBehavior: bottomSearch ? "horizontalOnly" : undefined, icon: { type: "sfSymbol", name: mailSearch.composeSystemImageName ?? "square.and.pencil" }, onPress: mailSearch.onComposePress, }); } + // Expanded Duo panes put toolbar actions in the side bar. Folded phones + // keep their bottom search toolbar. UIKit keeps the + // primary column's navigation items beside its title and the detail's in + // the rail; a conventional UIToolbar can otherwise leave them inaccessible. + if ( + nativeMetrics?.horizontalSizeClass === "regular" && + nativeMetrics.verticalBarEdge !== "none" + ) { + trailing.push(...bottom); + bottom.length = 0; + } const left = options.headerLeft?.(itemProps); const right = options.headerRight?.(itemProps); if (!options.unstable_headerLeftItems && isValidElement(left)) diff --git a/apps/mobile/src/native/createV5StackNavigator.ios.tsx b/apps/mobile/src/native/createV5StackNavigator.ios.tsx index 47edb3c1e816..9c8cc25f4102 100644 --- a/apps/mobile/src/native/createV5StackNavigator.ios.tsx +++ b/apps/mobile/src/native/createV5StackNavigator.ios.tsx @@ -24,6 +24,7 @@ import { useCallback, useRef, useState, type ComponentProps } from "react"; import { StyleSheet, View } from "react-native"; import { FormSheet, Stack } from "react-native-screens"; import { V5StackHeader } from "./V5StackHeader.ios"; +import type { AppNativeStackNavigationOptions } from "./StackHeader"; import { NativeColumnContent } from "./NativeColumnContent.ios"; import { nativeStackPopAction, @@ -232,7 +233,14 @@ export function V5CardStackView(props: V5StackViewProps) { : index > 0 } /> - {descriptor.render()} + + {descriptor.render()} + diff --git a/apps/mobile/src/native/use-native-mail-search-toolbar.ts b/apps/mobile/src/native/use-native-mail-search-toolbar.ts new file mode 100644 index 000000000000..b7791ff926a9 --- /dev/null +++ b/apps/mobile/src/native/use-native-mail-search-toolbar.ts @@ -0,0 +1,13 @@ +import { useNativeLayoutMetrics } from "./native-layout-metrics"; +import { NATIVE_LIQUID_GLASS_SUPPORTED } from "./native-glass"; + +/** Folded phones keep search in the bottom toolbar; expanded Duo panes use the side bar. */ +export function useNativeMailSearchToolbar() { + const metrics = useNativeLayoutMetrics(); + return ( + NATIVE_LIQUID_GLASS_SUPPORTED && + (metrics === null || + metrics.horizontalSizeClass === "compact" || + metrics.verticalBarEdge === "none") + ); +} diff --git a/docs/operations/mobile-screens-fork.md b/docs/operations/mobile-screens-fork.md index c62310462794..f0df0350f59e 100644 --- a/docs/operations/mobile-screens-fork.md +++ b/docs/operations/mobile-screens-fork.md @@ -1,7 +1,7 @@ # Building the mobile Screens dependency The mobile v5 stack uses a committed tarball from the -[T3 Screens fork](https://github.com/juliusmarminge/react-native-screens/tree/t3-v5.0.0-t3.7). +[T3 Screens fork](https://github.com/juliusmarminge/react-native-screens/tree/t3-v5.0.0-t3.8). The fork owns the integration changes; the app does not patch this package. Each archive includes compiled JavaScript, declarations, native source and `t3-fork.json` recording its version, source commit and upstream base. @@ -9,7 +9,7 @@ Each archive includes compiled JavaScript, declarations, native source and To reproduce the current archive with Node 24 and the repository's pinned Yarn: ```sh -git clone --branch t3-v5.0.0-t3.7 https://github.com/juliusmarminge/react-native-screens.git /tmp/t3-screens-fork +git clone --branch t3-v5.0.0-t3.8 https://github.com/juliusmarminge/react-native-screens.git /tmp/t3-screens-fork yarn --cwd /tmp/t3-screens-fork pack:t3 /absolute/path/to/t3code/apps/mobile/deps ``` @@ -20,3 +20,8 @@ Run `pack:t3` from that clean checkout and tag the source as `t3-v`. Replace the old archive, update `apps/mobile/package.json` and run `vp i` to regenerate the lockfile. Verify mobile types and affected navigation tests. Changes to native source also require rebuilding and testing the native client. + +The same iOS binary supports iPhone, iPad, and Duo. UIKit hinge updates enable +phone workspace columns at runtime; regular phones retain portrait rotation. +Use the normal EAS build profiles and update channels. Duo support requires a +binary built with the iOS 27.1 SDK; an OTA cannot add the native APIs. diff --git a/patches/@react-navigation%2Fnative-stack@7.17.6.patch b/patches/@react-navigation%2Fnative-stack@7.17.6.patch index 288bfcc7c7f0..ee4f4a0b5e32 100644 --- a/patches/@react-navigation%2Fnative-stack@7.17.6.patch +++ b/patches/@react-navigation%2Fnative-stack@7.17.6.patch @@ -67,7 +67,7 @@ index 9c21aa48cb580704aa17e59d9cdadcc612a1e4ff..a1b3a38368c5ef2257570f382e87df5d } return item; } -+ if (item.type === 'mailSearchToolbar') { ++ if (item.type === 'mailSearchToolbar' || item.type === 'searchBarPlacement') { + return { + ...item, + index @@ -81,7 +81,7 @@ index 9c21aa48cb580704aa17e59d9cdadcc612a1e4ff..a1b3a38368c5ef2257570f382e87df5d return processedItem; } - throw new Error(`Invalid item type: ${JSON.stringify(item)}. Valid types are 'button', 'menu', 'custom' and 'spacing'.`); -+ throw new Error(`Invalid item type: ${JSON.stringify(item)}. Valid types are 'button', 'menu', 'custom', 'spacing' and 'mailSearchToolbar'.`); ++ throw new Error(`Invalid item type: ${JSON.stringify(item)}. Valid types are 'button', 'menu', 'custom', 'spacing', 'mailSearchToolbar' and 'searchBarPlacement'.`); }).filter(item => item != null); }; const transformIcon = icon => { @@ -190,6 +190,10 @@ index 2f1351a89f0f2e67e854dc81e392198326b71834..ccf8556911fa1fa17588f06b56edb215 /** * A React Element to display as the item. */ +@@ -864,1 +864,3 @@ + sharesBackground?: boolean; ++ /** Native bar axis preference on iOS 27.1 and later. */ ++ axisBehavior?: 'automatic' | 'horizontalOnly' | 'verticalPreferred'; diff --git a/src/types.tsx b/src/types.tsx index 7488b1cf20afe36b5c585fbfcd369471fdb6ab8d..f4bd78e48c687024ba84e998fac79e9eb16f1a80 100644 --- a/src/types.tsx @@ -208,6 +212,10 @@ index 7488b1cf20afe36b5c585fbfcd369471fdb6ab8d..f4bd78e48c687024ba84e998fac79e9e /** * A React Element to display as the item. */ +@@ -920,1 +920,3 @@ + sharesBackground?: boolean; ++ /** Native bar axis preference on iOS 27.1 and later. */ ++ axisBehavior?: 'automatic' | 'horizontalOnly' | 'verticalPreferred'; diff --git a/src/views/useHeaderConfigProps.tsx b/src/views/useHeaderConfigProps.tsx index 3f3fff405048b3c2250afbe99dd7f35cecb9a6d5..bb062b898cfa77f8c02f951dc9d81b51672d7d0e 100644 --- a/src/views/useHeaderConfigProps.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cdee534c2d30..7612acfbbc4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,7 +154,7 @@ patchedDependencies: '@pierre/diffs@1.5.2': b33a575eb1382270ed0df956307e4b6ffe4b8e4349cefeb43e14f648c0fdc3b0 '@react-native-ai/apple@0.12.0': 2d09870c2848d185cb05b53ed823a46e12dba519324d8dd8e584e28731990f9d '@react-native-menu/menu@2.0.0': a4866ecfcd44f318043de839d20110c17e14909004900f002ffe12050376f2ba - '@react-navigation/native-stack@7.17.6': 3a53ec9b92f5db5f083f3ada585da75d62a13c82250c6ae47d92be91a828ad87 + '@react-navigation/native-stack@7.17.6': d2d52f3776e30765519748fd5e177592dc3b9cc778c52fb00e02674dfa9042c5 alchemy@2.0.0-beta.80: bea1b6c0c0b23157fd5f439a1d7feff430f28ac1621ba135d8a13eeec60ccacd dbus-next@0.10.2: cfff57561b0ee59b5addb3b2e6c6f20906e967507a530ab67e8db8108e520ba4 effect@4.0.2: ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688 @@ -346,7 +346,7 @@ importers: version: 7.3.4(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) '@react-navigation/native-stack': specifier: 7.17.6 - version: 7.17.6(patch_hash=3a53ec9b92f5db5f083f3ada585da75d62a13c82250c6ae47d92be91a828ad87)(141a742cd91f55378cde709a48a3af06) + version: 7.17.6(patch_hash=d2d52f3776e30765519748fd5e177592dc3b9cc778c52fb00e02674dfa9042c5)(cc0272b81e52c9a28f5defc327cee1b5) '@shikijs/core': specifier: 4.2.0 version: 4.2.0 @@ -549,8 +549,8 @@ importers: specifier: ~5.9.1 version: 5.9.1(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) react-native-screens: - specifier: file:deps/react-native-screens-5.0.0-t3.7.tgz - version: file:apps/mobile/deps/react-native-screens-5.0.0-t3.7.tgz(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) + specifier: file:deps/react-native-screens-5.0.0-t3.8.tgz + version: file:apps/mobile/deps/react-native-screens-5.0.0-t3.8.tgz(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) react-native-shiki-engine: specifier: ^0.3.12 version: 0.3.12(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) @@ -10798,9 +10798,9 @@ packages: react: '*' react-native: '*' - react-native-screens@file:apps/mobile/deps/react-native-screens-5.0.0-t3.7.tgz: - resolution: {integrity: sha512-Di1+YTCuhWpk9helKBxkvXAVAQTCSRo24tpFBGXdbTAT4vPm2vGPbtPe+lnIXcePjNHbTEaI1ha5n9PqQKl5lg==, tarball: file:apps/mobile/deps/react-native-screens-5.0.0-t3.7.tgz} - version: 5.0.0-t3.7 + react-native-screens@file:apps/mobile/deps/react-native-screens-5.0.0-t3.8.tgz: + resolution: {integrity: sha512-sgvyU7L9I1iKfFF5p/oe84duBpA2rJmY/WTgHgKmSI2JGLhWBWQ5qWjo/7WRyZkt36sjOkdNGxwNGhx7dCPzJA==, tarball: file:apps/mobile/deps/react-native-screens-5.0.0-t3.8.tgz} + version: 5.0.0-t3.8 peerDependencies: react: '*' react-native: '*' @@ -15971,7 +15971,7 @@ snapshots: optionalDependencies: '@react-native-masked-view/masked-view': 0.3.2(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) - '@react-navigation/native-stack@7.17.6(patch_hash=3a53ec9b92f5db5f083f3ada585da75d62a13c82250c6ae47d92be91a828ad87)(141a742cd91f55378cde709a48a3af06)': + '@react-navigation/native-stack@7.17.6(patch_hash=d2d52f3776e30765519748fd5e177592dc3b9cc778c52fb00e02674dfa9042c5)(cc0272b81e52c9a28f5defc327cee1b5)': dependencies: '@react-navigation/elements': 2.9.26(83de68753219ecf7f3321e065bd378be) '@react-navigation/native': 7.3.4(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) @@ -15979,7 +15979,7 @@ snapshots: react: 19.3.0 react-native: 0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6) react-native-safe-area-context: 5.9.1(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) - react-native-screens: file:apps/mobile/deps/react-native-screens-5.0.0-t3.7.tgz(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) + react-native-screens: file:apps/mobile/deps/react-native-screens-5.0.0-t3.8.tgz(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) sf-symbols-typescript: 2.2.0 warn-once: 0.1.1 transitivePeerDependencies: @@ -21967,7 +21967,7 @@ snapshots: react: 19.3.0 react-native: 0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6) - react-native-screens@file:apps/mobile/deps/react-native-screens-5.0.0-t3.7.tgz(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0): + react-native-screens@file:apps/mobile/deps/react-native-screens-5.0.0-t3.8.tgz(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0): dependencies: react: 19.3.0 react-freeze: 1.0.4(react@19.3.0) From 49f849ecce3667abab7ce9e6aaa42dfd2e3c45d7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 16:22:36 -0700 Subject: [PATCH 24/39] refactor(source-control): GitManager reads provider resolvers, not host kinds (#17617) Co-authored-by: Claude Opus 5.5 --- apps/server/src/git/GitManager.test.ts | 35 +++++-- apps/server/src/git/GitManager.ts | 92 ++++++++----------- .../SourceControlProviderRegistry.ts | 3 +- ...ts => gitHubChangeRequestTemplate.test.ts} | 4 +- .../src/server/SourceControlHost.ts | 2 + .../src/server/SourceControlProvider.ts | 39 ++++++++ .../ForgejoSourceControlProvider.test.ts | 80 ++++++++-------- .../server/ForgejoSourceControlProvider.ts | 9 ++ packages/source-control-github/package.json | 4 + .../src/server/GitHubSourceControlProvider.ts | 13 +++ .../src/server/gitHubChangeRequestTemplate.ts | 16 +++- 11 files changed, 190 insertions(+), 107 deletions(-) rename apps/server/src/sourceControl/{PrTemplateDetection.test.ts => gitHubChangeRequestTemplate.test.ts} (97%) rename apps/server/src/sourceControl/PrTemplateDetection.ts => packages/source-control-github/src/server/gitHubChangeRequestTemplate.ts (88%) diff --git a/apps/server/src/git/GitManager.test.ts b/apps/server/src/git/GitManager.test.ts index 24dd7e1d54ea..45f4ae96a133 100644 --- a/apps/server/src/git/GitManager.test.ts +++ b/apps/server/src/git/GitManager.test.ts @@ -44,6 +44,7 @@ import { } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; import { decodeGitHubPullRequestListJson } from "@t3tools/source-control-github/server/gitHubPullRequests"; +import * as GitHubChangeRequestTemplate from "@t3tools/source-control-github/server/gitHubChangeRequestTemplate"; import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli"; import type * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; import * as TestSourceControlHost from "@t3tools/source-control-testing/TestSourceControlHost"; @@ -55,6 +56,7 @@ import * as VcsProjectConfig from "../vcs/VcsProjectConfig.ts"; import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts"; import * as BackgroundPolicy from "../background/BackgroundPolicy.ts"; import * as GitWorkflowService from "./GitWorkflowService.ts"; +import * as ForgejoSourceControlProvider from "@t3tools/source-control-forgejo/server/ForgejoSourceControlProvider"; import * as GitLabSourceControlProvider from "@t3tools/source-control-gitlab/server/GitLabSourceControlProvider"; import { ForgejoPullRequestSchema, @@ -545,6 +547,13 @@ function createGitHubProviderWithFakeGh(scenario: FakeGhScenario = {}): { return { service: { kind: "github", + // The GitHub provider's own lookup rule and template convention, which GitManager reads + // instead of the kind. + headBranchProbe: ({ headSelectors }) => ({ + headSelectors: headSelectors.filter((selector) => !selector.includes(":")), + limit: 100, + }), + listChangeRequests: (input) => input.state === "open" ? execute({ @@ -745,7 +754,18 @@ function makeManager(input?: { ); const layerSourceControlRegistry = Layer.effect( SourceControlProviderRegistry.SourceControlProviderRegistry, - Effect.succeed(input?.sourceControlProvider ?? fakeGitHubProvider).pipe( + Effect.gen(function* () { + // GitHub reads its PR template with git, which the fake gh cannot answer; give the fake + // the package's real reader over the test repository's git. + const git = yield* GitVcsDriver.GitVcsDriver; + return ( + input?.sourceControlProvider ?? { + ...fakeGitHubProvider, + readChangeRequestTemplate: ({ cwd, treeish }: { cwd: string; treeish: string }) => + GitHubChangeRequestTemplate.detect(cwd, treeish, git.execute), + } + ); + }).pipe( Effect.map((provider) => SourceControlProviderRegistry.SourceControlProviderRegistry.of({ resolveLink: (input) => provider.resolveLink?.(input), @@ -769,9 +789,12 @@ function makeManager(input?: { runForThread: () => Effect.succeed({ status: "no-script" as const }), }, ), - layerVcsDriver, layerServerSettings, - ).pipe(Layer.provideMerge(layerSourceControlRegistry), Layer.provideMerge(NodeServices.layer)); + ).pipe( + Layer.provideMerge(layerSourceControlRegistry), + Layer.provideMerge(layerVcsDriver), + Layer.provideMerge(NodeServices.layer), + ); // Built into the test's scope: the manager reads these stores after this returns. const layerStores = Layer.merge(ProjectionStore.layer, ProjectStore.layer).pipe( Layer.provideMerge(SqlitePersistence.layerMemory), @@ -4232,7 +4255,7 @@ it.layer(layerGitManagerTest)("GitManager", (it) => { }; const repository = GitManager.parseRepositoryNameWithOwnerFromRemoteUrl( `https://forgejo.example/forgejo/${owner}/project.git`, - "forgejo", + { repositoryNameFromRemoteUrl: ForgejoSourceControlProvider.repositoryNameFromRemoteUrl }, ); expect(repository).toBe(`${owner}/project`); const context = { @@ -4253,13 +4276,13 @@ it.layer(layerGitManagerTest)("GitManager", (it) => { expect( GitManager.parseRepositoryNameWithOwnerFromRemoteUrl( "git@forgejo.example:maria/project.git", - "forgejo", + { repositoryNameFromRemoteUrl: ForgejoSourceControlProvider.repositoryNameFromRemoteUrl }, ), ).toBe("maria/project"); expect( GitManager.parseRepositoryNameWithOwnerFromRemoteUrl( "https://gitlab.example/group/maria/project.git", - "gitlab", + {}, ), ).toBe("group/maria/project"); }), diff --git a/apps/server/src/git/GitManager.ts b/apps/server/src/git/GitManager.ts index 593910ba9a4a..8ca0b6b976a9 100644 --- a/apps/server/src/git/GitManager.ts +++ b/apps/server/src/git/GitManager.ts @@ -35,7 +35,6 @@ import { ModelSelection, type ProjectId, SourceControlProviderError, - type SourceControlProviderKind, type SourceControlWritingStyleSettings, type ThreadId, type ThreadPullRequestKey, @@ -81,8 +80,8 @@ import { detachStackFrame } from "./detachStackFrame.ts"; import * as ServerSettings from "../serverSettings.ts"; import type { GitManagerServiceError } from "@t3tools/contracts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; +import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts"; -import { detectPrTemplate } from "../sourceControl/PrTemplateDetection.ts"; import type { ChangeRequest } from "@t3tools/contracts"; export interface GitActionProgressReporter { @@ -314,27 +313,20 @@ function resolvePullRequestWorktreeLocalBranchName( return `t3code/pr-${pullRequest.number}/${suffix}`; } +/** The repository's `owner/name` from a remote URL, as `provider` reads its remote paths. */ export function parseRepositoryNameWithOwnerFromRemoteUrl( url: string | null, - providerKind?: ChangeRequest["provider"], + provider?: Pick< + SourceControlProvider.SourceControlProvider["Service"], + "repositoryNameFromRemoteUrl" + >, ): string | null { const trimmed = url?.trim() ?? ""; - if (trimmed.length === 0) { - return null; - } - - const match = - /^(?:[^@/\s]+@[^:/\s]+:|(?:ssh|https?|git):\/\/[^/]+\/)((?:[^/\s]+\/)+[^/\s]+?)(?:\.git)?\/?$/iu.exec( - trimmed, - ); - const repositoryNameWithOwner = match?.[1]?.trim() ?? ""; - // Forgejo HTTP paths can include an installation mount; its API always names owner/repo. - if (providerKind === "forgejo" && /^https?:\/\//iu.test(trimmed)) { - return repositoryNameWithOwner.length > 0 - ? repositoryNameWithOwner.split("/").slice(-2).join("/") - : null; - } - return repositoryNameWithOwner.length > 0 ? repositoryNameWithOwner : null; + if (trimmed.length === 0) return null; + return ( + provider?.repositoryNameFromRemoteUrl?.(trimmed) ?? + SourceControlProvider.repositoryPathFromRemoteUrl(trimmed) + ); } function parseRepositoryOwnerLogin(nameWithOwner: string | null): string | null { @@ -625,24 +617,18 @@ function parseCustomCommitMessage(raw: string): { subject: string; body: string }; } -// Without the owner selector, a bare branch name also lists same-named -// branches on other forks (`main`, `patch-1`), so GitHub probes ask for a full -// page and let matchesBranchHeadContext pick the right head. gh fetches up to -// 100 in one request, and GitHub prices a first:100 connection like first:1. -const GITHUB_HEAD_BRANCH_PROBE_LIMIT = 100; - -// `gh pr list --head` filters on the head ref name alone and accepts anything, so an -// `owner:branch` or `remote:branch` selector silently lists zero pull requests -// while spending a GraphQL call. Git branch names cannot contain ":", and the -// bare head branch is always among the selectors, so GitHub probes skip them and -// leave the owner check to matchesBranchHeadContext. -function probeableHeadSelectors( - providerKind: SourceControlProviderKind, +/** The selectors and page size `provider` asks about when looking up a branch's change requests. */ +function headBranchProbe( + provider: SourceControlProvider.SourceControlProvider["Service"], headSelectors: ReadonlyArray, -): ReadonlyArray { - return providerKind === "github" - ? headSelectors.filter((selector) => !selector.includes(":")) - : headSelectors; + state: "open" | "all", +) { + return ( + provider.headBranchProbe?.({ headSelectors, state }) ?? { + headSelectors, + limit: state === "open" ? 1 : 20, + } + ); } function appendUnique(values: string[], next: string | null | undefined): void { @@ -1430,15 +1416,14 @@ export const make = Effect.gen(function* () { /^https?:\/\//iu.test(remoteUrl) && (repositoryNameWithOwner?.split("/").length ?? 0) > 2 ) { - const detected = detectSourceControlProviderFromGitRemoteUrl(remoteUrl); - const kind = - detected?.kind === "unknown" - ? yield* sourceControlProvider(cwd).pipe( - Effect.map((provider) => provider.kind), - Effect.orElseSucceed(() => undefined), - ) - : detected?.kind; - repositoryNameWithOwner = parseRepositoryNameWithOwnerFromRemoteUrl(remoteUrl, kind); + const kind = detectSourceControlProviderFromGitRemoteUrl(remoteUrl)?.kind; + const provider = + kind === undefined + ? undefined + : yield* ( + kind === "unknown" ? sourceControlProvider(cwd) : sourceControlProviders.get(kind) + ).pipe(Effect.orElseSucceed(() => undefined)); + repositoryNameWithOwner = parseRepositoryNameWithOwnerFromRemoteUrl(remoteUrl, provider); } return { remoteUrlKey: remoteUrl ? normalizeGitRemoteUrl(remoteUrl) : null, @@ -1701,13 +1686,13 @@ export const make = Effect.gen(function* () { >, ) { const provider = yield* sourceControlProvider(cwd); - const headSelectors = probeableHeadSelectors(provider.kind, headContext.headSelectors); - for (const headSelector of headSelectors) { + const probe = headBranchProbe(provider, headContext.headSelectors, "open"); + for (const headSelector of probe.headSelectors) { const pullRequests = yield* provider.listChangeRequests({ cwd, headSelector, state: "open", - limit: provider.kind === "github" ? GITHUB_HEAD_BRANCH_PROBE_LIMIT : 1, + limit: probe.limit, }); const normalizedPullRequests = pullRequests.map(toPullRequestInfo); @@ -1733,12 +1718,13 @@ export const make = Effect.gen(function* () { const parsedByNumber = new Map(); const provider = yield* sourceControlProvider(cwd); - for (const headSelector of probeableHeadSelectors(provider.kind, headContext.headSelectors)) { + const probe = headBranchProbe(provider, headContext.headSelectors, "all"); + for (const headSelector of probe.headSelectors) { const pullRequests = yield* provider.listChangeRequests({ cwd, headSelector, state: "all", - limit: provider.kind === "github" ? GITHUB_HEAD_BRANCH_PROBE_LIMIT : 20, + limit: probe.limit, }); for (const pr of pullRequests.map(toPullRequestInfo)) { @@ -2129,8 +2115,10 @@ export const make = Effect.gen(function* () { const rangeContext = yield* gitCore.readRangeContext(cwd, baseRangeRef); const policy = yield* resolveStylePolicy(cwd, settings); const changeRequestTemplate = - settings.style.followChangeRequestTemplates && provider.kind === "github" - ? Option.getOrUndefined(yield* detectPrTemplate(cwd, baseRangeRef, gitCore.execute)) + settings.style.followChangeRequestTemplates && provider.readChangeRequestTemplate + ? Option.getOrUndefined( + yield* provider.readChangeRequestTemplate({ cwd, treeish: baseRangeRef }), + ) : undefined; const generated = yield* textGeneration.generatePrContent({ diff --git a/apps/server/src/sourceControl/SourceControlProviderRegistry.ts b/apps/server/src/sourceControl/SourceControlProviderRegistry.ts index 153e8c21b113..7af90663dc93 100644 --- a/apps/server/src/sourceControl/SourceControlProviderRegistry.ts +++ b/apps/server/src/sourceControl/SourceControlProviderRegistry.ts @@ -158,8 +158,7 @@ function bindProviderContext( } return SourceControlProvider.SourceControlProvider.of({ - kind: provider.kind, - ...(provider.resolveLink ? { resolveLink: provider.resolveLink } : {}), + ...provider, listChangeRequests: (input) => provider.listChangeRequests({ ...input, diff --git a/apps/server/src/sourceControl/PrTemplateDetection.test.ts b/apps/server/src/sourceControl/gitHubChangeRequestTemplate.test.ts similarity index 97% rename from apps/server/src/sourceControl/PrTemplateDetection.test.ts rename to apps/server/src/sourceControl/gitHubChangeRequestTemplate.test.ts index 53972f019f24..1c7c34d2ab80 100644 --- a/apps/server/src/sourceControl/PrTemplateDetection.test.ts +++ b/apps/server/src/sourceControl/gitHubChangeRequestTemplate.test.ts @@ -9,7 +9,7 @@ import * as Path from "effect/Path"; import * as ServerConfig from "../config.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; -import { detectPrTemplate } from "./PrTemplateDetection.ts"; +import * as GitHubChangeRequestTemplate from "@t3tools/source-control-github/server/gitHubChangeRequestTemplate"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; const SINGLE_TEMPLATE_PATHS = [ @@ -80,7 +80,7 @@ const commitTemplates = (cwd: string) => const detectTemplate = (cwd: string, treeish = "HEAD") => Effect.gen(function* () { const git = yield* GitVcsDriver.GitVcsDriver; - return yield* detectPrTemplate(cwd, treeish, git.execute); + return yield* GitHubChangeRequestTemplate.detect(cwd, treeish, git.execute); }); it.effect.each(SINGLE_TEMPLATE_PATHS)("recognizes $0", (relativePath) => diff --git a/packages/source-control-core/src/server/SourceControlHost.ts b/packages/source-control-core/src/server/SourceControlHost.ts index a46db0fccf27..442dc47d0c68 100644 --- a/packages/source-control-core/src/server/SourceControlHost.ts +++ b/packages/source-control-core/src/server/SourceControlHost.ts @@ -83,6 +83,8 @@ export class SourceControlHost extends Context.Service< readonly operation: string; readonly cwd: string; readonly args: ReadonlyArray; + readonly maxOutputBytes?: number; + readonly appendTruncationMarker?: boolean; }) => Effect.Effect; readonly resolveCommit: (input: { readonly cwd: string; diff --git a/packages/source-control-core/src/server/SourceControlProvider.ts b/packages/source-control-core/src/server/SourceControlProvider.ts index 2bcabceb00e3..74b946651faa 100644 --- a/packages/source-control-core/src/server/SourceControlProvider.ts +++ b/packages/source-control-core/src/server/SourceControlProvider.ts @@ -1,5 +1,6 @@ import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; +import type * as Option from "effect/Option"; import type { ChangeRequest, ChangeRequestState, @@ -92,10 +93,48 @@ export function sourceControlRefFromInput(input: { return input.source ?? parseSourceControlOwnerRef(input.headSelector); } +/** The repository path a remote URL names (`owner/name`, or deeper for nested groups). */ +export function repositoryPathFromRemoteUrl(url: string | null): string | null { + const trimmed = url?.trim() ?? ""; + if (trimmed.length === 0) { + return null; + } + + const match = + /^(?:[^@/\s]+@[^:/\s]+:|(?:ssh|https?|git):\/\/[^/]+\/)((?:[^/\s]+\/)+[^/\s]+?)(?:\.git)?\/?$/iu.exec( + trimmed, + ); + const path = match?.[1]?.trim() ?? ""; + return path.length > 0 ? path : null; +} + export class SourceControlProvider extends Context.Service< SourceControlProvider, { readonly kind: SourceControlProviderKind; + /** + * How to look up a branch's change requests: which head selectors to ask about, and how many + * results to read per selector. The caller checks the owner against what comes back, so a + * host that cannot search `owner:branch` can drop those selectors without losing anything. + * Absent asks about every selector, reading 1 for the open lookup and 20 for any state. + */ + readonly headBranchProbe?: (input: { + readonly headSelectors: ReadonlyArray; + readonly state: "open" | "all"; + }) => { readonly headSelectors: ReadonlyArray; readonly limit: number }; + /** + * The repository's change request template at `treeish`, for change request content + * generation to follow. Absent means the host has no template convention. + */ + readonly readChangeRequestTemplate?: (input: { + readonly cwd: string; + readonly treeish: string; + }) => Effect.Effect>; + /** + * The repository's `owner/name` from a remote URL, for a host whose remote paths can carry + * more than that (Forgejo's HTTP installation mount). Absent means the whole path. + */ + readonly repositoryNameFromRemoteUrl?: (url: string) => string | null; /** Optional capability for issue and change-request subjects. */ readonly resolveLink?: ResolveSourceControlLink; readonly listChangeRequests: (input: { diff --git a/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts b/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts index 679a4e69028c..bdddd5c94b40 100644 --- a/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts +++ b/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts @@ -1,13 +1,12 @@ import { assert, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; -import type * as Context from "effect/Context"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; -import * as FetchHttpClient from "effect/http/FetchHttpClient"; import * as HttpClient from "effect/http/HttpClient"; +import * as HttpClientRequest from "effect/http/HttpClientRequest"; import * as HttpClientResponse from "effect/http/HttpClientResponse"; import { VcsProcessSpawnError } from "@t3tools/contracts"; import { firstNonEmptyLine } from "@t3tools/source-control-core/server/discovery"; @@ -20,38 +19,45 @@ import * as ForgejoSourceControlProvider from "./ForgejoSourceControlProvider.ts const processOutput = TestSourceControlHost.processOutput; const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); +const decodeJson = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown)); + +/** The JSON body a request sends. Forgejo encodes bodies as UTF-8 text. */ +const requestJson = (request: HttpClientRequest.HttpClientRequest): unknown => { + assert.strictEqual(request.body._tag, "Uint8Array"); + return request.body._tag === "Uint8Array" + ? decodeJson(new TextDecoder().decode(request.body.body)) + : undefined; +}; const encodeJsonEffect = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); it.effect("submits a Forgejo review without sending its summary in the preliminary GET", () => { const methods: string[] = []; - const fetchReview = async ( - ...[input, init]: Parameters> - ) => { - const request = new Request(input instanceof Request ? input.url : String(input), { - ...(init?.method === undefined ? {} : { method: init.method }), - ...(init?.headers === undefined ? {} : { headers: init.headers }), - ...(init?.body === undefined ? {} : { body: init.body }), - }); + const reviewClient = HttpClient.make((request) => { methods.push(request.method); if (request.method === "GET") { assert.strictEqual(request.url, "https://forgejo.test/api/v1/repos/maria/project/pulls/42"); - return new Response( - encodeJson({ - number: 42, - title: "Review target", - body: "", - html_url: "https://forgejo.test/maria/project/pulls/42", - user: { login: "maria" }, - state: "open", - merged: false, - head: { ref: "feature", sha: "head", repo: null }, - base: { ref: "main", sha: "base", repo: null }, - created_at: "2026-09-13T00:00:00Z", - updated_at: "2026-09-13T00:00:00Z", - closed_at: null, - merged_at: null, - labels: [], - }), + return Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response( + encodeJson({ + number: 42, + title: "Review target", + body: "", + html_url: "https://forgejo.test/maria/project/pulls/42", + user: { login: "maria" }, + state: "open", + merged: false, + head: { ref: "feature", sha: "head", repo: null }, + base: { ref: "main", sha: "base", repo: null }, + created_at: "2026-09-13T00:00:00Z", + updated_at: "2026-09-13T00:00:00Z", + closed_at: null, + merged_at: null, + labels: [], + }), + ), + ), ); } assert.strictEqual(request.method, "POST"); @@ -59,14 +65,16 @@ it.effect("submits a Forgejo review without sending its summary in the prelimina request.url, "https://forgejo.test/api/v1/repos/maria/project/pulls/42/reviews", ); - assert.deepStrictEqual(JSON.parse(await request.text()), { + assert.deepStrictEqual(requestJson(request), { event: "COMMENT", body: "Review summary", commit_id: "head", comments: [], }); - return new Response('{"id":1}', { status: 200 }); - }; + return Effect.succeed( + HttpClientResponse.fromWeb(request, new Response('{"id":1}', { status: 200 })), + ); + }); return Effect.gen(function* () { const cli = yield* ForgejoCli.make; const provider = yield* ForgejoPullRequestProvider.make.pipe( @@ -83,11 +91,7 @@ it.effect("submits a Forgejo review without sending its summary in the prelimina }); assert.deepStrictEqual(methods, ["GET", "POST"]); }).pipe( - Effect.provideService( - FetchHttpClient.Fetch, - Object.assign(fetchReview, { preconnect: () => undefined }), - ), - Effect.provide(FetchHttpClient.layer), + Effect.provideService(HttpClient.HttpClient, reviewClient), Effect.provideService( FileSystem.FileSystem, FileSystem.makeNoop({ @@ -837,11 +841,7 @@ it.effect("prefers fj for HTTP and ported SSH aliases on root servers", () => { } assert.strictEqual(request.method, "POST"); assert.strictEqual(request.headers.authorization, "token test-token"); - assert.strictEqual(request.body._tag, "Uint8Array"); - if (request.body._tag === "Uint8Array") - assert.deepStrictEqual(JSON.parse(new TextDecoder().decode(request.body.body)), { - body: "verified through fj", - }); + assert.deepStrictEqual(requestJson(request), { body: "verified through fj" }); return Effect.succeed( HttpClientResponse.fromWeb(request, new Response('{"id":99}', { status: 201 })), ); diff --git a/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.ts b/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.ts index cbcb3405f4ec..86196975f2a3 100644 --- a/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.ts +++ b/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.ts @@ -158,6 +158,14 @@ export const makeDiscovery = Effect.gen(function* () { } satisfies SourceControlManagedCliDiscoverySpec; }); +/** HTTP paths can include an installation mount; Forgejo's API always names owner/repo. */ +export function repositoryNameFromRemoteUrl(url: string): string | null { + const path = SourceControlProvider.repositoryPathFromRemoteUrl(url); + return path === null || !/^https?:\/\//iu.test(url.trim()) + ? path + : path.split("/").slice(-2).join("/"); +} + const RepositorySchema = Schema.Struct({ full_name: Schema.String, clone_url: Schema.String, @@ -228,6 +236,7 @@ export const make = Effect.gen(function* () { }); return SourceControlProvider.SourceControlProvider.of({ kind: "forgejo", + repositoryNameFromRemoteUrl, listChangeRequests: (input) => Effect.gen(function* () { const repo = yield* cli.resolveRepository(input); diff --git a/packages/source-control-github/package.json b/packages/source-control-github/package.json index 739d6e8b5f3f..46a8965976fd 100644 --- a/packages/source-control-github/package.json +++ b/packages/source-control-github/package.json @@ -15,6 +15,10 @@ "types": "./src/server/gitHubAuthStatus.ts", "import": "./src/server/gitHubAuthStatus.ts" }, + "./server/gitHubChangeRequestTemplate": { + "types": "./src/server/gitHubChangeRequestTemplate.ts", + "import": "./src/server/gitHubChangeRequestTemplate.ts" + }, "./server/gitHubConditionalChecks": { "types": "./src/server/gitHubConditionalChecks.ts", "import": "./src/server/gitHubConditionalChecks.ts" diff --git a/packages/source-control-github/src/server/GitHubSourceControlProvider.ts b/packages/source-control-github/src/server/GitHubSourceControlProvider.ts index 0f442f65d712..f906f3e629d4 100644 --- a/packages/source-control-github/src/server/GitHubSourceControlProvider.ts +++ b/packages/source-control-github/src/server/GitHubSourceControlProvider.ts @@ -23,6 +23,7 @@ import { decodeJsonResult } from "@t3tools/shared/schemaJson"; import { isSshRemoteUrl } from "@t3tools/shared/sourceControl"; import * as GitHubApi from "./GitHubApi.ts"; +import * as GitHubChangeRequestTemplate from "./gitHubChangeRequestTemplate.ts"; import { decodeGitHubPullRequestEntries, type NormalizedGitHubPullRequestRecord, @@ -939,6 +940,18 @@ export const make = Effect.gen(function* () { return SourceControlProvider.SourceControlProvider.of({ kind: "github", + // `gh pr list --head` filters on the head ref name alone and accepts anything, so an + // `owner:branch` or `remote:branch` selector silently lists zero pull requests while + // spending a GraphQL call; the bare branch is always among the selectors. Without the owner, + // a bare branch also lists same-named branches on other forks (`main`, `patch-1`), so read a + // full page and let the owner check pick the right head. gh fetches up to 100 in one + // request, and GitHub prices a first:100 connection like first:1. + headBranchProbe: ({ headSelectors }) => ({ + headSelectors: headSelectors.filter((selector) => !selector.includes(":")), + limit: 100, + }), + readChangeRequestTemplate: ({ cwd, treeish }) => + GitHubChangeRequestTemplate.detect(cwd, treeish, git.execute), resolveLink: (input) => { // Automatic enrichment must not send ambient CLI credentials to a host from message text. if (input.url.host !== "github.com") return undefined; diff --git a/apps/server/src/sourceControl/PrTemplateDetection.ts b/packages/source-control-github/src/server/gitHubChangeRequestTemplate.ts similarity index 88% rename from apps/server/src/sourceControl/PrTemplateDetection.ts rename to packages/source-control-github/src/server/gitHubChangeRequestTemplate.ts index 6872708fec94..e6915c6bf01e 100644 --- a/apps/server/src/sourceControl/PrTemplateDetection.ts +++ b/packages/source-control-github/src/server/gitHubChangeRequestTemplate.ts @@ -1,8 +1,14 @@ +/** + * GitHub's pull request template convention: the first single template found at a known path, + * or the only Markdown file in a template directory, read from the committed base tree. + * + * @module source-control-github/server/gitHubChangeRequestTemplate + */ import type { GitCommandError } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; -import type * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; +import type * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; const TEMPLATE_MAX_BYTES = 8_000; const TREE_LIST_MAX_BYTES = 100_000; @@ -25,7 +31,7 @@ const TEMPLATE_DIRECTORIES = [ const TREE_PATHS = [...TEMPLATE_PATHS, ...TEMPLATE_DIRECTORIES] as const; -type ExecuteGit = GitVcsDriver.GitVcsDriver["Service"]["execute"]; +type ExecuteGit = SourceControlHost.SourceControlHost["Service"]["git"]["execute"]; interface TemplateTreeEntry { readonly objectId: string; @@ -66,7 +72,7 @@ function readTemplateBlob(input: { }): Effect.Effect, GitCommandError> { return input .executeGit({ - operation: "PrTemplateDetection.readTemplateBlob", + operation: "gitHubChangeRequestTemplate.readTemplateBlob", cwd: input.cwd, args: ["cat-file", "blob", input.entry.objectId], maxOutputBytes: TEMPLATE_MAX_BYTES, @@ -125,7 +131,7 @@ function readTemplateDirectory(input: { }); } -export const detectPrTemplate = Effect.fn("detectPrTemplate")(function* ( +export const detect = Effect.fn("gitHubChangeRequestTemplate.detect")(function* ( cwd: string, treeish: string, executeGit: ExecuteGit, @@ -134,7 +140,7 @@ export const detectPrTemplate = Effect.fn("detectPrTemplate")(function* ( // Worktree paths can be replaced between validation and open. Read regular blobs from the // committed base tree so repository-controlled symlinks and path races never reach the host filesystem. const result = yield* executeGit({ - operation: "PrTemplateDetection.listTemplates", + operation: "gitHubChangeRequestTemplate.listTemplates", cwd, args: ["ls-tree", "-r", "-z", "--full-tree", treeish, "--", ...TREE_PATHS], maxOutputBytes: TREE_LIST_MAX_BYTES, From 2ce3e4ae74deb63bda7aaf059949a12aaaefed56 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 16:22:36 -0700 Subject: [PATCH 25/39] refactor(source-control): PullRequestService reads GitHub resolvers, not its kind (#17619) Co-authored-by: Claude Opus 5.5 --- .../pullRequest/PullRequestService.test.ts | 2 ++ .../src/pullRequest/PullRequestService.ts | 19 ++++++++++++------- .../src/server/PullRequestProvider.ts | 9 ++++++++- .../src/server/SourceControlRateLimit.ts | 10 ++++++++++ .../src/server/GitHubApi.test.ts | 4 ++-- .../src/server/GitHubApi.ts | 16 +++------------- .../src/server/GitHubPullRequestApi.test.ts | 7 ++++--- .../src/server/GitHubPullRequestApi.ts | 2 +- .../server/GitHubPullRequestProvider.test.ts | 3 ++- .../src/server/GitHubPullRequestProvider.ts | 6 ++++-- .../src/server/GitHubSourceControlProvider.ts | 2 +- 11 files changed, 49 insertions(+), 31 deletions(-) diff --git a/apps/server/src/pullRequest/PullRequestService.test.ts b/apps/server/src/pullRequest/PullRequestService.test.ts index 83f362fa20a0..04d4c879a656 100644 --- a/apps/server/src/pullRequest/PullRequestService.test.ts +++ b/apps/server/src/pullRequest/PullRequestService.test.ts @@ -463,6 +463,8 @@ function fakeProvider( setReaction: () => Effect.void, listReviewerCandidates: () => Effect.succeed({ candidates: [], truncated: false }), setReviewerRequest: () => Effect.void, + // GitHub's own merge message rewrite, which the service reads instead of the kind. + ...(kind === "github" ? { mergeMessageRewrite: (message: string) => message } : {}), ...overrides, }; } diff --git a/apps/server/src/pullRequest/PullRequestService.ts b/apps/server/src/pullRequest/PullRequestService.ts index 0295673350b7..3644fefe985a 100644 --- a/apps/server/src/pullRequest/PullRequestService.ts +++ b/apps/server/src/pullRequest/PullRequestService.ts @@ -77,7 +77,6 @@ import { import { resolveProjectSettings } from "@t3tools/shared/projectSettings"; import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; -import { AllowGitHubReserve } from "@t3tools/source-control-github/server/GitHubApi"; import * as ProjectService from "../project/ProjectService.ts"; import * as ServerSettings from "../serverSettings.ts"; import * as PullRequestFilesViewed from "../persistence/PullRequestFilesViewed.ts"; @@ -557,7 +556,7 @@ function withRateLimitBackoff( ), Effect.flatMap((lease) => effect.pipe( - Effect.provideService(AllowGitHubReserve, allowPaused), + Effect.provideService(SourceControlRateLimit.Interactive, allowPaused), Effect.tap(() => limits.recordSuccess({ ...key, lease })), Effect.tapError((error) => error.reason === "rate-limited" @@ -587,6 +586,9 @@ function withRateLimitBackoff( const wrapped = { kind: api.kind, capabilities: api.capabilities, + ...(api.mergeMessageRewrite === undefined + ? {} + : { mergeMessageRewrite: api.mergeMessageRewrite }), // Refused during a pause like any other read, except for the caller that asks for the // bypass: a lookup that failed is not held, so letting every background read through would // spawn this host's CLI on each of them and re-extend the pause it was already in. @@ -1558,8 +1560,10 @@ export const make = Effect.gen(function* () { )(function* (input) { const host = input.host.toLowerCase(); const { supported } = yield* listWorkspaceProjects({ host }); - const project = supported.find((candidate) => candidate.api.kind === "github"); - const api = registry.get("github"); + const project = supported.find( + (candidate) => registry.get(candidate.api.kind)?.getRoutingIdentity !== undefined, + ); + const api = project === undefined ? null : registry.get(project.api.kind); if (project === undefined || api?.getRoutingIdentity === undefined) { return yield* new PullRequestUnavailableError({ reason: "provider-unsupported" }); } @@ -1569,6 +1573,7 @@ export const make = Effect.gen(function* () { host, }) .pipe(Effect.mapError(toPullRequestError("routeIdentity"))); + // Only GitHub reports a routing identity, and the contract names it. return { ...identity, host, provider: "github" as const }; }); @@ -1584,7 +1589,7 @@ export const make = Effect.gen(function* () { detail: "The GitHub account could not be verified before starting the operation.", }); const project = yield* requireProject(input).pipe(Effect.mapError(rejected)); - const api = project.api.kind === "github" ? registry.get("github") : null; + const api = registry.get(project.api.kind); if ( api?.withVerifiedCredential === undefined || input.host?.toLowerCase() !== project.host.toLowerCase() @@ -1605,7 +1610,7 @@ export const make = Effect.gen(function* () { const routing = Effect.fn("PullRequestService.routing")(function* (input: PullRequestRef) { const project = yield* requireProject(input); - const api = project.api.kind === "github" ? registry.get("github") : null; + const api = registry.get(project.api.kind); if (api?.getRoutingIdentity === undefined) { return yield* new PullRequestUnavailableError({ reason: "provider-unsupported" }); } @@ -2047,7 +2052,7 @@ export const make = Effect.gen(function* () { ); } const mergeSettings = - project.api.kind === "github" && + project.api.mergeMessageRewrite !== undefined && input.stackNumber === undefined && (input.action === "merge" || input.action === "enable-auto-merge") ? serverSettings.getSettings.pipe( diff --git a/packages/source-control-core/src/server/PullRequestProvider.ts b/packages/source-control-core/src/server/PullRequestProvider.ts index c3f588a62277..2500c8fae62a 100644 --- a/packages/source-control-core/src/server/PullRequestProvider.ts +++ b/packages/source-control-core/src/server/PullRequestProvider.ts @@ -345,6 +345,13 @@ export interface PullRequestProviderApi { >; readonly kind: SourceControlProviderKind; readonly capabilities: PullRequestCapabilities; + /** + * Rewrites the message a merge will use, for a host that lets the merge carry custom text. + * Absent means the host always writes its own message, so nothing is read to decide on one. + * Today's only rewrite strips agent credits (`mergeMessage.removeAgentCredits`) when the + * project asks for it. + */ + readonly mergeMessageRewrite?: (message: string) => string; /** The signed-in account, which is what involvement filtering compares against. */ readonly getViewer: (input: { @@ -561,7 +568,7 @@ export interface PullRequestProviderApi { readonly action: PullRequestAction; readonly stackNumber?: number; readonly expectedStackHeads?: ReadonlyArray; - /** GitHub merge message cleanup; ignored by hosts without support. */ + /** Apply `mergeMessageRewrite` to the merge message; never sent to a host without one. */ readonly removeAgentCreditsOnMerge?: boolean; /** Meaningful for `merge` and `enable-auto-merge`; absent takes the host's own default. */ readonly mergeMethod?: PullRequestMergeMethod; diff --git a/packages/source-control-core/src/server/SourceControlRateLimit.ts b/packages/source-control-core/src/server/SourceControlRateLimit.ts index f2ef07dd2529..262a975eacf5 100644 --- a/packages/source-control-core/src/server/SourceControlRateLimit.ts +++ b/packages/source-control-core/src/server/SourceControlRateLimit.ts @@ -20,6 +20,16 @@ export const CredentialScope = Context.Reference( }, ); +/** + * Set by interactive callers (a user's read or write, not a background sweep). A host may let + * requests made under it spend a reserved quota and go through a rate-limit pause: a user acting + * on a change request should not be refused because a background read exhausted the quota. + */ +export const Interactive = Context.Reference( + "@t3tools/source-control-core/server/SourceControlRateLimit/Interactive", + { defaultValue: () => false }, +); + interface RateLimitKey { readonly provider: SourceControlProviderKind; readonly host: string; diff --git a/packages/source-control-github/src/server/GitHubApi.test.ts b/packages/source-control-github/src/server/GitHubApi.test.ts index 3287ebb8e1a1..e77f10ec9370 100644 --- a/packages/source-control-github/src/server/GitHubApi.test.ts +++ b/packages/source-control-github/src/server/GitHubApi.test.ts @@ -275,7 +275,7 @@ describe("GitHubApi", () => { retryAt: reset * 1000, }); // A user's own request may spend the reserve, and GraphQL has a quota of its own. - yield* api.rest(read).pipe(Effect.provideService(GitHubApi.AllowGitHubReserve, true)); + yield* api.rest(read).pipe(Effect.provideService(SourceControlRateLimit.Interactive, true)); yield* api.graphql({ host: "github.com", operation: "x", query: "query { viewer { id } }" }); expect(requests).toHaveLength(3); // The reset gives the background its quota back. @@ -344,7 +344,7 @@ describe("GitHubApi", () => { method: "PUT", path: "repos/acme/web/pulls/7/merge", }) - .pipe(Effect.provideService(GitHubApi.AllowGitHubReserve, true)); + .pipe(Effect.provideService(SourceControlRateLimit.Interactive, true)); expect(merged.status).toBe(200); expect(requests).toHaveLength(2); }).pipe(Effect.provide(layer)); diff --git a/packages/source-control-github/src/server/GitHubApi.ts b/packages/source-control-github/src/server/GitHubApi.ts index e734e5ff07a5..111281a0484e 100644 --- a/packages/source-control-github/src/server/GitHubApi.ts +++ b/packages/source-control-github/src/server/GitHubApi.ts @@ -34,16 +34,6 @@ export const PinnedGitHubCredential = Context.Reference<{ defaultValue: () => null, }); -/** - * Set by interactive callers (a user's read or write, not a background sweep). Requests made - * under it may spend the GraphQL reserve and go through a rate-limit pause: a user acting on a - * pull request should not be refused because a background read exhausted the quota. - */ -export const AllowGitHubReserve = Context.Reference( - "@t3tools/source-control-github/server/GitHubApi/AllowGitHubReserve", - { defaultValue: () => false }, -); - export class GitHubApiRequestError extends Schema.TaggedError()( "GitHubApiRequestError", { host: Schema.String, operation: Schema.String, cause: Schema.Defect() }, @@ -137,7 +127,7 @@ export interface GitHubRestInput { readonly maxResponseBytes?: number; /** Defaults to 30 seconds; a whole pull request's patch may need longer. */ readonly timeout?: Duration.Input; - /** Overrides `AllowGitHubReserve` for this one request. */ + /** Overrides `SourceControlRateLimit.Interactive` for this one request. */ readonly allowReserve?: boolean; } @@ -525,7 +515,7 @@ export const make = Effect.gen(function* () { input.body === undefined ? withEtag : withEtag.pipe(HttpClientRequest.bodyJsonUnsafe(input.body)); - return AllowGitHubReserve.pipe( + return SourceControlRateLimit.Interactive.pipe( Effect.flatMap((interactive) => send({ host: input.host, @@ -545,7 +535,7 @@ export const make = Effect.gen(function* () { const host = normalizeHost(input.host); const { fingerprint } = yield* credential(host); const scope = (yield* SourceControlRateLimit.CredentialScope) || fingerprint; - const allowReserve = input.allowReserve ?? (yield* AllowGitHubReserve); + const allowReserve = input.allowReserve ?? (yield* SourceControlRateLimit.Interactive); // The document, never its variables: user text (bodies, search terms) travels as variables. yield* Effect.annotateCurrentSpan({ "github.operation": input.operation, diff --git a/packages/source-control-github/src/server/GitHubPullRequestApi.test.ts b/packages/source-control-github/src/server/GitHubPullRequestApi.test.ts index badb79677585..961106c58acc 100644 --- a/packages/source-control-github/src/server/GitHubPullRequestApi.test.ts +++ b/packages/source-control-github/src/server/GitHubPullRequestApi.test.ts @@ -12,7 +12,6 @@ import * as Redacted from "effect/Redacted"; import { HttpClient, HttpClientResponse } from "effect/http"; -import { AllowGitHubReserve } from "./GitHubApi.ts"; import * as GitHubApi from "./GitHubApi.ts"; import * as GitHubCredentials from "./GitHubCredentials.ts"; import * as GitHubQuota from "./GitHubQuota.ts"; @@ -78,7 +77,7 @@ const mockApi = Layer.effect( const quota = yield* GitHubQuota.GitHubQuota; return GitHubApi.GitHubApi.of({ graphql: (input) => - GitHubApi.AllowGitHubReserve.pipe( + SourceControlRateLimit.Interactive.pipe( Effect.flatMap((interactive) => quota.admit(input.host, "graphql", { allowReserve: input.allowReserve ?? interactive, @@ -3864,7 +3863,9 @@ layer("GitHubPullRequestApi.layer", (it) => { const error = yield* Effect.flip(cli.getPullRequestDetail(input)); expect(error._tag).toBe("SourceControlRateLimitPausedError"); expect(mockedExecute).toHaveBeenCalledOnce(); - yield* cli.getPullRequestDetail(input).pipe(Effect.provideService(AllowGitHubReserve, true)); + yield* cli + .getPullRequestDetail(input) + .pipe(Effect.provideService(SourceControlRateLimit.Interactive, true)); expect(mockedExecute).toHaveBeenCalledTimes(2); }), ); diff --git a/packages/source-control-github/src/server/GitHubPullRequestApi.ts b/packages/source-control-github/src/server/GitHubPullRequestApi.ts index ceff9c6c0b5c..b8c7fe227688 100644 --- a/packages/source-control-github/src/server/GitHubPullRequestApi.ts +++ b/packages/source-control-github/src/server/GitHubPullRequestApi.ts @@ -1526,7 +1526,7 @@ export const make = Effect.gen(function* () { const getPullRequestDetail: GitHubPullRequestApi["Service"]["getPullRequestDetail"] = (input) => { const { owner, name } = parseRepositorySelector(input.repository); - return GitHubApi.AllowGitHubReserve.pipe( + return SourceControlRateLimit.Interactive.pipe( Effect.flatMap((allowReserve) => graphqlRead({ allowReserve, diff --git a/packages/source-control-github/src/server/GitHubPullRequestProvider.test.ts b/packages/source-control-github/src/server/GitHubPullRequestProvider.test.ts index 98e8aeb5c15b..781d07f0d496 100644 --- a/packages/source-control-github/src/server/GitHubPullRequestProvider.test.ts +++ b/packages/source-control-github/src/server/GitHubPullRequestProvider.test.ts @@ -7,6 +7,7 @@ import type { PullRequestReaction } from "@t3tools/contracts"; import { decodePullRequestDetailJson } from "./gitHubPullRequestJson.ts"; import * as GitHubApi from "./GitHubApi.ts"; +import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; import * as GitHubPullRequestApi from "./GitHubPullRequestApi.ts"; import type { GitHubPullRequestCore } from "./gitHubPullRequestJson.ts"; import { gitHubViewerPermissions, loginAvatarUrl, make } from "./GitHubPullRequestProvider.ts"; @@ -725,7 +726,7 @@ describe("getViewerPermissions", () => { Layer.mock(GitHubPullRequestApi.GitHubPullRequestApi)({ revalidateChecks: (_input, read) => read, getPullRequestDetail: () => - GitHubApi.AllowGitHubReserve.pipe( + SourceControlRateLimit.Interactive.pipe( Effect.tap((allowReserve) => Effect.sync(() => onDetail(allowReserve))), Effect.flatMap(() => detail), ), diff --git a/packages/source-control-github/src/server/GitHubPullRequestProvider.ts b/packages/source-control-github/src/server/GitHubPullRequestProvider.ts index a3030f6c86ad..7b74bd368b81 100644 --- a/packages/source-control-github/src/server/GitHubPullRequestProvider.ts +++ b/packages/source-control-github/src/server/GitHubPullRequestProvider.ts @@ -1,3 +1,4 @@ +import { removeAgentCredits } from "@t3tools/source-control-core/server/mergeMessage"; import * as Effect from "effect/Effect"; import type { PullRequestActor, @@ -7,7 +8,7 @@ import type { PullRequestViewerPermissions, } from "@t3tools/contracts"; -import * as GitHubApi from "./GitHubApi.ts"; +import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; import * as GitHubPullRequestApi from "./GitHubPullRequestApi.ts"; import { PullRequestProviderError, @@ -262,6 +263,7 @@ export const make = Effect.gen(function* () { const provider: PullRequestProviderApi = { kind: "github", capabilities: CAPABILITIES, + mergeMessageRewrite: removeAgentCredits, getRoutingIdentity: (input) => cli.getRoutingIdentity(input).pipe(Effect.mapError(fail("routeIdentity"))), withVerifiedCredential: (input, use) => @@ -526,7 +528,7 @@ export const make = Effect.gen(function* () { // comparison, so one read usually answers what used to take three. When that heavier read // fails, the light access read still answers, withholding only update-branch. return cli.getPullRequestDetail(input).pipe( - Effect.provideService(GitHubApi.AllowGitHubReserve, true), + Effect.provideService(SourceControlRateLimit.Interactive, true), Effect.map((pullRequest) => gitHubViewerPermissions({ ...pullRequest.viewerAccess, diff --git a/packages/source-control-github/src/server/GitHubSourceControlProvider.ts b/packages/source-control-github/src/server/GitHubSourceControlProvider.ts index f906f3e629d4..8edf9423c7c9 100644 --- a/packages/source-control-github/src/server/GitHubSourceControlProvider.ts +++ b/packages/source-control-github/src/server/GitHubSourceControlProvider.ts @@ -963,7 +963,7 @@ export const make = Effect.gen(function* () { }, listChangeRequests: (input) => // An open lookup is a user waiting on a status; the rest may be a background sweep. - (input.state === "open" ? Effect.succeed(true) : GitHubApi.AllowGitHubReserve).pipe( + (input.state === "open" ? Effect.succeed(true) : SourceControlRateLimit.Interactive).pipe( Effect.flatMap((allowReserve) => listByHead({ cwd: input.cwd, From 51347aca3aa73112bc1e288ae45507ce515f42a2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 16:22:37 -0700 Subject: [PATCH 26/39] refactor(source-control): Forgejo identity and Azure DevOps addressing move into their packages (#17624) Co-authored-by: Claude Opus 5.5 --- .../pullRequest/PullRequestService.test.ts | 5 +- .../src/pullRequest/PullRequestService.ts | 37 ++++++------ apps/server/src/server.ts | 40 +++++-------- .../server/AzureDevOpsPullRequestProvider.ts | 2 + .../src/server/PullRequestProvider.ts | 13 ++++ .../src/server/SourceControlProvider.ts | 13 ++++ .../ForgejoSourceControlProvider.test.ts | 60 +++++++++++++++++++ .../server/ForgejoSourceControlProvider.ts | 38 ++++++++++++ 8 files changed, 162 insertions(+), 46 deletions(-) diff --git a/apps/server/src/pullRequest/PullRequestService.test.ts b/apps/server/src/pullRequest/PullRequestService.test.ts index 04d4c879a656..c74faa92f06d 100644 --- a/apps/server/src/pullRequest/PullRequestService.test.ts +++ b/apps/server/src/pullRequest/PullRequestService.test.ts @@ -463,8 +463,11 @@ function fakeProvider( setReaction: () => Effect.void, listReviewerCandidates: () => Effect.succeed({ candidates: [], truncated: false }), setReviewerRequest: () => Effect.void, - // GitHub's own merge message rewrite, which the service reads instead of the kind. + // The hosts' own resolvers, which the service reads instead of the kind. ...(kind === "github" ? { mergeMessageRewrite: (message: string) => message } : {}), + ...(kind === "azure-devops" + ? { repositoryKey: ({ canonicalKey }: { readonly canonicalKey: string }) => canonicalKey } + : {}), ...overrides, }; } diff --git a/apps/server/src/pullRequest/PullRequestService.ts b/apps/server/src/pullRequest/PullRequestService.ts index 3644fefe985a..b44d713c3158 100644 --- a/apps/server/src/pullRequest/PullRequestService.ts +++ b/apps/server/src/pullRequest/PullRequestService.ts @@ -369,6 +369,12 @@ export interface SupportedProject { * Unique where `repository` is not: Azure's is a bare name that repeats across an organisation. */ readonly remote: string; + /** + * The host's own key for this checkout's repository (`api.repositoryKey`), normalized, or null + * where `owner/name` on the host identifies it. When set, only a reference whose key matches + * is served by this checkout. + */ + readonly repositoryKey: string | null; } /** @@ -589,6 +595,7 @@ function withRateLimitBackoff( ...(api.mergeMessageRewrite === undefined ? {} : { mergeMessageRewrite: api.mergeMessageRewrite }), + ...(api.repositoryKey === undefined ? {} : { repositoryKey: api.repositoryKey }), // Refused during a pause like any other read, except for the caller that asks for the // bypass: a lookup that failed is not held, so letting every background read through would // spawn this host's CLI on each of them and re-extend the pause it was already in. @@ -852,10 +859,9 @@ export const make = Effect.gen(function* () { if (roots === undefined) viewerRoots.set(host, [project.workspaceRoot]); else if (!roots.includes(project.workspaceRoot)) roots.push(project.workspaceRoot); } - const key = listCursorKey( - host, - kind === "azure-devops" ? identity.canonicalKey : repository, - ); + const repositoryKey = + api?.repositoryKey?.({ canonicalKey: identity.canonicalKey }) ?? null; + const key = listCursorKey(host, repositoryKey ?? repository); if (seen.has(key)) continue; seen.add(key); if (api === null) { @@ -870,10 +876,9 @@ export const make = Effect.gen(function* () { api: withRateLimitBackoff(api, host, rateLimits), repository, host, - remote: - kind === "azure-devops" - ? identity.canonicalKey - : normalizeGitRemoteUrl(`https://${host}/${repository}`), + remote: repositoryKey ?? normalizeGitRemoteUrl(`https://${host}/${repository}`), + repositoryKey: + repositoryKey === null ? null : canonicalRepositoryKey(repositoryKey.toLowerCase()), }); } return { supported, unimplemented, viewerRoots }; @@ -922,25 +927,21 @@ export const make = Effect.gen(function* () { const route = supported.find( (candidate) => - candidate.api.kind === "azure-devops" && - candidate.project.repositoryIdentity != null && - canonicalRepositoryKey( - candidate.project.repositoryIdentity.canonicalKey.toLowerCase(), - ) === repositoryKey, + candidate.repositoryKey !== null && candidate.repositoryKey === repositoryKey, ) ?? onHost.find( (candidate) => - candidate.api.kind !== "azure-devops" && + candidate.repositoryKey === null && candidate.repository.toLowerCase() === repository.toLowerCase(), ) ?? - onHost.find((candidate) => candidate.api.kind !== "azure-devops"); + onHost.find((candidate) => candidate.repositoryKey === null); if (route === undefined) { return Effect.fail( new PullRequestUnavailableError({ reason: "provider-unsupported" }), ); } return Effect.succeed( - route.api.kind === "azure-devops" || + route.repositoryKey !== null || route.repository.toLowerCase() === repository.toLowerCase() ? route : { @@ -2099,9 +2100,7 @@ export const make = Effect.gen(function* () { ), Effect.mapError(toPullRequestError("runAction")), Effect.as( - project.api.kind === "azure-devops" - ? input.repository.trim() - : project.repository, + project.repositoryKey !== null ? input.repository.trim() : project.repository, ), ), ), diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 80c5a73ac4c1..e24cb335b487 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -52,7 +52,6 @@ import * as ProviderHostLive from "./provider/ProviderHostLive.ts"; import * as AcpRegistrySupport from "@t3tools/provider-acp-registry/server/AcpRegistrySupport"; import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts"; import * as CheckpointStore from "./checkpointing/CheckpointStore.ts"; -import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; import * as SourceControlBuiltInDrivers from "./sourceControl/builtInDrivers.ts"; import * as TextGeneration from "./textGeneration/TextGeneration.ts"; import * as ProviderInstanceRegistryHydration from "./provider/ProviderInstanceRegistryHydration.ts"; @@ -315,32 +314,21 @@ const layerRepositoryIdentityResolver = Layer.effect( Effect.gen(function* () { const registry = yield* SourceControlProviderRegistry.SourceControlProviderRegistry; return yield* RepositoryIdentityResolver.make({ + // Each host that can refine an identity gets a turn; the first one that changes it wins. refine: Effect.fn(function* (identity: RepositoryIdentity) { - const remote = ForgejoCli.parseForgejoRemote(identity.locator.remoteUrl); - if ( - !remote || - !identity.rootPath || - (identity.provider !== undefined && - identity.provider !== "unknown" && - identity.provider !== "forgejo") - ) - return identity; - const handle = yield* registry.resolveHandle({ - cwd: identity.rootPath, - context: { - provider: { kind: "unknown", name: "Unknown", baseUrl: "" }, - remoteName: identity.locator.remoteName, - remoteUrl: identity.locator.remoteUrl, - }, - }); - if (handle.context?.provider.kind !== "forgejo") return identity; - const baseUrl = handle.context.provider.baseUrl.replace(/\/+$/, ""); - const basePath = new URL(baseUrl).pathname.replace(/^\/+|\/+$/g, ""); - const path = - !remote.ssh && basePath && remote.path.startsWith(`${basePath}/`) - ? remote.path.slice(basePath.length + 1) - : remote.path; - return { ...identity, provider: "forgejo", webUrl: `${baseUrl}/${path}` }; + for (const kind of SourceControlBuiltInDrivers.BUILT_IN_SOURCE_CONTROL_DRIVERS.map( + (driver) => driver.kind, + )) { + const provider = yield* registry.get(kind); + if (provider.refineRepositoryIdentity === undefined) continue; + const refined = yield* provider.refineRepositoryIdentity({ + identity, + resolveContext: (input) => + registry.resolveHandle(input).pipe(Effect.map((handle) => handle.context)), + }); + if (refined !== identity) return refined; + } + return identity; }), }); }), diff --git a/packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestProvider.ts b/packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestProvider.ts index afd045636947..b30356bba7b5 100644 --- a/packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestProvider.ts +++ b/packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestProvider.ts @@ -298,6 +298,8 @@ export const make = Effect.gen(function* () { const provider: PullRequestProviderApi = { kind: "azure-devops", capabilities: CAPABILITIES, + // The bare repository name repeats across an organization; the project path does not. + repositoryKey: ({ canonicalKey }) => canonicalKey, getViewer: (input) => cli.getViewer({ cwd: input.cwd }).pipe(Effect.mapError(fail("getViewer"))), diff --git a/packages/source-control-core/src/server/PullRequestProvider.ts b/packages/source-control-core/src/server/PullRequestProvider.ts index 2500c8fae62a..85f5d1893ffb 100644 --- a/packages/source-control-core/src/server/PullRequestProvider.ts +++ b/packages/source-control-core/src/server/PullRequestProvider.ts @@ -352,6 +352,19 @@ export interface PullRequestProviderApi { * project asks for it. */ readonly mergeMessageRewrite?: (message: string) => string; + /** + * The key a checkout's repository is known by on this host, where `owner/name` is not enough. + * Absent means `owner/name` on the host identifies it, and any checkout on the host can serve a + * change request in another repository there. + * + * A host that resolves it (Azure DevOps: `dev.azure.com/org/project/_git/repo`) derives its + * organization from the checkout, so only a checkout whose key matches the reference serves + * it. The key also names the repository in list cursors and on routing. + */ + readonly repositoryKey?: (input: { + /** The checkout's canonical key, as its remote names it. */ + readonly canonicalKey: string; + }) => string; /** The signed-in account, which is what involvement filtering compares against. */ readonly getViewer: (input: { diff --git a/packages/source-control-core/src/server/SourceControlProvider.ts b/packages/source-control-core/src/server/SourceControlProvider.ts index 74b946651faa..44d4cf4e95c5 100644 --- a/packages/source-control-core/src/server/SourceControlProvider.ts +++ b/packages/source-control-core/src/server/SourceControlProvider.ts @@ -2,6 +2,7 @@ import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import type * as Option from "effect/Option"; import type { + RepositoryIdentity, ChangeRequest, ChangeRequestState, SourceControlProviderError, @@ -135,6 +136,18 @@ export class SourceControlProvider extends Context.Service< * more than that (Forgejo's HTTP installation mount). Absent means the whole path. */ readonly repositoryNameFromRemoteUrl?: (url: string) => string | null; + /** + * Fills in what a repository identity read from git cannot know, such as the browser URL of + * a host the remote URL does not name. `resolveContext` asks the registry which host (and + * base URL) serves a remote. Only consulted for identities this host may own. + */ + readonly refineRepositoryIdentity?: (input: { + readonly identity: RepositoryIdentity; + readonly resolveContext: (input: { + readonly cwd: string; + readonly context: SourceControlProviderContext; + }) => Effect.Effect; + }) => Effect.Effect; /** Optional capability for issue and change-request subjects. */ readonly resolveLink?: ResolveSourceControlLink; readonly listChangeRequests: (input: { diff --git a/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts b/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts index bdddd5c94b40..e3dae3a6c529 100644 --- a/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts +++ b/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts @@ -245,6 +245,66 @@ it.effect.each([ }, ); +it.effect("gives Forgejo identities the browser URL of the login that serves them", () => + Effect.gen(function* () { + const provider = yield* ForgejoSourceControlProvider.make; + const refine = provider.refineRepositoryIdentity; + assert.isDefined(refine); + const identity = (remoteUrl: string, provider?: string) => ({ + canonicalKey: "forge.test/team/repo", + locator: { source: "git-remote" as const, remoteName: "origin", remoteUrl }, + rootPath: "/repo", + ...(provider === undefined ? {} : { provider }), + }); + const served = (baseUrl: string) => () => + Effect.succeed({ + provider: { kind: "forgejo" as const, name: "Forgejo / Gitea", baseUrl }, + remoteName: "origin", + remoteUrl: "", + }); + + // An installation mount in the HTTP path is the login's, not the repository's. + assert.deepStrictEqual( + yield* refine!({ + identity: identity("http://forge.test:3000/git/team/repo.git"), + resolveContext: served("http://forge.test:3000/git/"), + }), + { + ...identity("http://forge.test:3000/git/team/repo.git"), + provider: "forgejo", + webUrl: "http://forge.test:3000/git/team/repo", + }, + ); + // An SSH alias keeps its whole path. + assert.strictEqual( + (yield* refine!({ + identity: identity("ssh://git@ssh.forge.test/team/repo.git"), + resolveContext: served("http://forge.test:3000/git"), + })).webUrl, + "http://forge.test:3000/git/team/repo", + ); + // Another host's identity, or a remote no Forgejo login serves, is left alone. + const github = identity("https://github.com/team/repo.git", "github"); + assert.strictEqual( + yield* refine!({ identity: github, resolveContext: () => Effect.die("not asked") }), + github, + ); + const unserved = identity("http://forge.test:3000/team/repo.git"); + assert.strictEqual( + yield* refine!({ identity: unserved, resolveContext: () => Effect.succeed(null) }), + unserved, + ); + }).pipe( + Effect.provide( + Layer.mergeAll( + Layer.succeed(FileSystem.FileSystem, FileSystem.makeNoop({})), + TestSourceControlHost.layer(), + Layer.mock(ForgejoCli.ForgejoCli)({}), + ), + ), + ), +); + it.effect("loads Forgejo pull request references from files and commits views", () => Effect.gen(function* () { const provider = yield* ForgejoSourceControlProvider.make; diff --git a/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.ts b/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.ts index 86196975f2a3..fbcc5fbee99d 100644 --- a/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.ts +++ b/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.ts @@ -166,6 +166,43 @@ export function repositoryNameFromRemoteUrl(url: string): string | null { : path.split("/").slice(-2).join("/"); } +/** + * A Forgejo remote's URL can't say which server it belongs to (an SSH alias, an installation + * mount), so the identity's browser URL comes from the login that serves it. + */ +const refineRepositoryIdentity: NonNullable< + SourceControlProvider.SourceControlProvider["Service"]["refineRepositoryIdentity"] +> = Effect.fn("ForgejoSourceControlProvider.refineRepositoryIdentity")(function* ({ + identity, + resolveContext, +}) { + const remote = ForgejoCli.parseForgejoRemote(identity.locator.remoteUrl); + if ( + !remote || + !identity.rootPath || + (identity.provider !== undefined && + identity.provider !== "unknown" && + identity.provider !== "forgejo") + ) + return identity; + const context = yield* resolveContext({ + cwd: identity.rootPath, + context: { + provider: { kind: "unknown", name: "Unknown", baseUrl: "" }, + remoteName: identity.locator.remoteName, + remoteUrl: identity.locator.remoteUrl, + }, + }); + if (context?.provider.kind !== "forgejo") return identity; + const baseUrl = context.provider.baseUrl.replace(/\/+$/, ""); + const basePath = new URL(baseUrl).pathname.replace(/^\/+|\/+$/g, ""); + const path = + !remote.ssh && basePath && remote.path.startsWith(`${basePath}/`) + ? remote.path.slice(basePath.length + 1) + : remote.path; + return { ...identity, provider: "forgejo", webUrl: `${baseUrl}/${path}` }; +}); + const RepositorySchema = Schema.Struct({ full_name: Schema.String, clone_url: Schema.String, @@ -237,6 +274,7 @@ export const make = Effect.gen(function* () { return SourceControlProvider.SourceControlProvider.of({ kind: "forgejo", repositoryNameFromRemoteUrl, + refineRepositoryIdentity, listChangeRequests: (input) => Effect.gen(function* () { const repo = yield* cli.resolveRepository(input); From 58916b6554f45ac518bd8150dd10f1f536e40aac Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 16:38:41 -0700 Subject: [PATCH 27/39] refactor: home directory comes from a HostProcessHomeDirectory reference (#17628) Co-authored-by: Claude Opus 5.5 --- apps/desktop/src/app/DesktopApp.ts | 5 +- .../src/app/DesktopPreReadyPlatform.ts | 26 ++++---- apps/desktop/src/ipc/methods/window.ts | 4 +- .../snapShot/CaptureShortcutConfig.test.ts | 16 ++--- .../src/snapShot/CaptureShortcutConfig.ts | 9 +-- apps/desktop/src/snapShot/DesktopSnapShot.ts | 14 ++-- .../src/snapShot/HyprlandSnapShot.test.ts | 2 +- apps/desktop/src/snapShot/HyprlandSnapShot.ts | 3 +- apps/server/src/assets/AssetAccess.test.ts | 14 +--- apps/server/src/assets/AssetAccess.ts | 3 +- apps/server/src/cli/app.test.ts | 22 ++----- .../Adapters/ClaudeAdapterV2.ts | 6 +- .../Adapters/CodexAdapterV2.ts | 6 +- .../Adapters/GrokAdapterV2.test.ts | 36 ++++++---- .../orchestration-v2/workflowScriptQuery.ts | 9 +-- apps/server/src/os-jank.ts | 13 ++-- .../server/src/project/AgentSessionScanner.ts | 24 ++++--- apps/server/src/provider/CodexProvider.ts | 5 +- .../src/provider/Drivers/AntigravityDriver.ts | 10 ++- .../Drivers/AntigravitySkills.test.ts | 28 ++++++-- .../src/provider/Drivers/AntigravitySkills.ts | 7 +- .../src/provider/Drivers/AntigravityUsage.ts | 7 +- .../src/provider/Drivers/ClaudeDriver.ts | 5 +- .../server/src/provider/Drivers/ClaudeHome.ts | 8 +-- .../src/provider/Drivers/ClaudeSkills.ts | 8 +-- .../src/provider/Drivers/CodexDriver.ts | 5 +- .../src/provider/Drivers/CodexHomeLayout.ts | 14 ++-- .../provider/Drivers/CodexManagedProvider.ts | 2 +- .../src/provider/Drivers/claudeUsage.ts | 7 +- .../provider/ProviderInstanceRegistry.test.ts | 9 ++- .../src/provider/antigravityAuthSupport.ts | 9 ++- .../server/src/provider/claudeResetCredits.ts | 15 +++-- .../src/provider/providerInstallation.ts | 27 ++++---- apps/server/src/review/ReviewService.ts | 4 +- .../SourceControlRepositoryService.ts | 3 +- apps/server/src/storageCleanup.ts | 2 + apps/server/src/telemetry/Identify.ts | 7 +- apps/server/src/terminal/Manager.ts | 18 +++-- .../src/textGeneration/CodexTextGeneration.ts | 8 ++- apps/server/src/usage/UsageService.ts | 46 +++++++------ apps/server/src/vcs/GitVcsDriverCore.ts | 3 +- apps/server/src/workspace/WorkspaceEntries.ts | 9 ++- apps/server/src/workspace/WorkspacePaths.ts | 5 +- apps/server/src/worktreesDirectory.ts | 9 ++- .../src/server/adapter.ts | 2 +- .../src/server/driver.ts | 5 +- .../src/server/AcpSessionRuntime.ts | 5 +- .../provider-acp/src/server/stderr.test.ts | 3 + packages/provider-acp/src/server/stderr.ts | 6 +- .../src/server/instanceEnvironment.test.ts | 66 ++++++++++--------- .../src/server/instanceEnvironment.ts | 11 ++-- .../src/server/pathExpansion.test.ts | 19 +++--- .../provider-core/src/server/pathExpansion.ts | 16 ++--- .../src/server/CursorUsageAccounts.ts | 9 ++- .../provider-cursor/src/server/adapter.ts | 2 +- packages/provider-cursor/src/server/driver.ts | 2 +- packages/provider-cursor/src/server/skills.ts | 7 +- .../src/server/textGeneration.ts | 4 +- .../provider-cursor/src/server/usageLimits.ts | 6 +- packages/provider-grok/src/server/adapter.ts | 16 +++-- packages/provider-grok/src/server/driver.ts | 2 +- packages/provider-grok/src/server/usage.ts | 6 +- .../provider-grok/src/server/usageLimits.ts | 7 +- .../src/server/xaiAcpExtension.test.ts | 10 +-- .../src/server/xaiAcpExtension.ts | 11 ++-- packages/provider-muse/src/server/driver.ts | 6 +- .../provider-opencode/src/server/adapter.ts | 2 +- .../provider-opencode/src/server/driver.ts | 2 +- .../provider-opencode/src/server/usage.ts | 8 +-- .../src/server/usageLimits.ts | 9 ++- packages/provider-pi/src/server/adapter.ts | 2 +- packages/provider-pi/src/server/driver.ts | 2 +- packages/shared/src/hostProcess.ts | 7 ++ .../src/server/ForgejoCli.ts | 5 +- 74 files changed, 442 insertions(+), 308 deletions(-) diff --git a/apps/desktop/src/app/DesktopApp.ts b/apps/desktop/src/app/DesktopApp.ts index 944077d0e396..b76f6b4ba197 100644 --- a/apps/desktop/src/app/DesktopApp.ts +++ b/apps/desktop/src/app/DesktopApp.ts @@ -35,6 +35,7 @@ import * as DesktopUpdates from "../updates/DesktopUpdates.ts"; import * as DesktopSnapShot from "../snapShot/DesktopSnapShot.ts"; import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts"; import * as DesktopRendererHistory from "../telemetry/DesktopRendererHistory.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const DEFAULT_DESKTOP_BACKEND_PORT = 3773; const MAX_TCP_PORT = 65_535; @@ -284,7 +285,9 @@ const startup = Effect.gen(function* () { preReadyElectronOptions.linuxPasswordStoreCommandLine !== null; const linuxElectronOptions = environment.platform === "linux" && !hasCommandLinePasswordStore - ? DesktopPreReadyPlatform.resolveEarlyLinuxElectronOptionsFromProcess() + ? DesktopPreReadyPlatform.resolveEarlyLinuxElectronOptionsFromProcess( + yield* HostProcessHomeDirectory, + ) : preReadyElectronOptions.linux; if (linuxElectronOptions !== null && !hasCommandLinePasswordStore) { if ( diff --git a/apps/desktop/src/app/DesktopPreReadyPlatform.ts b/apps/desktop/src/app/DesktopPreReadyPlatform.ts index 9b73a5d825bf..2de132887aa2 100644 --- a/apps/desktop/src/app/DesktopPreReadyPlatform.ts +++ b/apps/desktop/src/app/DesktopPreReadyPlatform.ts @@ -1,13 +1,12 @@ // @effect-diagnostics nodeBuiltinImport:off - pre-ready Electron setup reads settings and prepares the Linux desktop entry synchronously before app services are available. import * as NodeFS from "node:fs"; -import * as NodeOS from "node:os"; import * as NodePath from "node:path"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Electron from "electron"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as DesktopEarlyElectronStartup from "./DesktopEarlyElectronStartup.ts"; import { resolveDesktopAppBranding } from "./DesktopEnvironment.ts"; @@ -31,14 +30,15 @@ function readCommandLineSwitchValue( return value.length > 0 ? value : null; } -export const resolveEarlyLinuxElectronOptionsFromProcess = - (): DesktopEarlyElectronStartup.EarlyLinuxElectronOptions => - DesktopEarlyElectronStartup.resolveEarlyLinuxElectronOptions({ - env: process.env, - homeDirectory: NodeOS.homedir(), - joinPath: NodePath.posix.join, - readFileString: (path) => NodeFS.readFileSync(path, "utf8"), - }); +export const resolveEarlyLinuxElectronOptionsFromProcess = ( + homeDirectory: string, +): DesktopEarlyElectronStartup.EarlyLinuxElectronOptions => + DesktopEarlyElectronStartup.resolveEarlyLinuxElectronOptions({ + env: process.env, + homeDirectory, + joinPath: NodePath.posix.join, + readFileString: (path) => NodeFS.readFileSync(path, "utf8"), + }); export class DesktopPreReadyElectronOptions extends Context.Service< DesktopPreReadyElectronOptions, @@ -51,12 +51,14 @@ export class DesktopPreReadyElectronOptions extends Context.Service< /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { const platform = yield* HostProcessPlatform; + const homeDirectory = yield* HostProcessHomeDirectory; return yield* Effect.sync((): DesktopPreReadyElectronOptions["Service"] => { const linuxPasswordStoreCommandLine = platform === "linux" ? readCommandLineSwitchValue(Electron.app.commandLine, "password-store") : null; - const linux = platform === "linux" ? resolveEarlyLinuxElectronOptionsFromProcess() : null; + const linux = + platform === "linux" ? resolveEarlyLinuxElectronOptionsFromProcess(homeDirectory) : null; if (linux !== null) { // The portal also requires a valid desktop entry. An AppImage update may @@ -64,7 +66,7 @@ export const make = Effect.gen(function* () { try { const applicationsDir = NodePath.posix.join( process.env.XDG_DATA_HOME?.trim() || - NodePath.posix.join(NodeOS.homedir(), ".local", "share"), + NodePath.posix.join(homeDirectory, ".local", "share"), "applications", ); NodeFS.mkdirSync(applicationsDir, { recursive: true }); diff --git a/apps/desktop/src/ipc/methods/window.ts b/apps/desktop/src/ipc/methods/window.ts index 64e0566d3e70..8e4ffebfefe7 100644 --- a/apps/desktop/src/ipc/methods/window.ts +++ b/apps/desktop/src/ipc/methods/window.ts @@ -16,7 +16,6 @@ import { import { WORKSPACE_IMAGE_PREVIEW_EXTENSIONS } from "@t3tools/shared/filePreview"; import { resolveEditorCommand } from "@t3tools/shared/editor"; import * as HostProcess from "@t3tools/shared/hostProcess"; -import * as NodeOS from "node:os"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Effect from "effect/Effect"; @@ -46,6 +45,7 @@ import { resolveWslPickFolderDefaultPath, wslUncPathToLinuxPath, } from "../../wsl/wslPathParsing.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const ContextMenuPosition = Schema.Struct({ x: Schema.Number, @@ -398,7 +398,7 @@ export const pickThemeFiles = DesktopIpc.makeIpcMethod({ // The VS Code extensions directory is the same dotfolder on Windows, // macOS, and Linux; when it is missing the picker opens wherever the // platform would by default. - const extensionsDir = path.join(NodeOS.homedir(), ".vscode", "extensions"); + const extensionsDir = path.join(yield* HostProcessHomeDirectory, ".vscode", "extensions"); const defaultPath = yield* fileSystem .exists(extensionsDir) .pipe(Effect.orElseSucceed(() => false)); diff --git a/apps/desktop/src/snapShot/CaptureShortcutConfig.test.ts b/apps/desktop/src/snapShot/CaptureShortcutConfig.test.ts index 0b9346a9f8cf..248a6874e48f 100644 --- a/apps/desktop/src/snapShot/CaptureShortcutConfig.test.ts +++ b/apps/desktop/src/snapShot/CaptureShortcutConfig.test.ts @@ -29,7 +29,7 @@ beforeEach(async () => { tools.validateNiri.mockReset().mockResolvedValue(undefined); tools.hyprlandBindings.mockReset().mockResolvedValue([]); tools.reloadHyprland.mockReset().mockResolvedValue(undefined); - setup = new CaptureShortcutConfig(tools); + setup = new CaptureShortcutConfig(NodeOS.homedir(), tools); }); afterEach(async () => { await NodeFSP.rm(directory, { recursive: true, force: true }); @@ -219,15 +219,15 @@ it("does not edit Omarchy's shipped defaults", async () => { }); it("honors NIRI_CONFIG and XDG_CONFIG_HOME", () => { expect( - niriCaptureConfigPath( - { NIRI_CONFIG: "/custom/test.kdl", XDG_CONFIG_HOME: "/config" }, - "/home/test", - ), + niriCaptureConfigPath("/home/test", { + NIRI_CONFIG: "/custom/test.kdl", + XDG_CONFIG_HOME: "/config", + }), ).toBe("/custom/test.kdl"); - expect(niriCaptureConfigPath({ XDG_CONFIG_HOME: "/config" }, "/home/test")).toBe( + expect(niriCaptureConfigPath("/home/test", { XDG_CONFIG_HOME: "/config" })).toBe( "/config/niri/config.kdl", ); - expect(niriCaptureConfigPath({}, "/home/test")).toBe("/home/test/.config/niri/config.kdl"); + expect(niriCaptureConfigPath("/home/test", {})).toBe("/home/test/.config/niri/config.kdl"); }); it("preserves a UTF-8 BOM through preview and apply", async () => { await NodeFSP.writeFile(path, "\uFEFFbinds {}\r\n"); @@ -251,7 +251,7 @@ it("the installed Niri parser accepts the generated edit and its relative includ NodePath.join(directory, "keys.kdl"), "binds {\n Ctrl+Alt+Q { quit; }\n}\n", ); - const checked = new CaptureShortcutConfig({ + const checked = new CaptureShortcutConfig(NodeOS.homedir(), { ...tools, validateNiri: async (file) => { await NodeUtil.promisify(NodeChildProcess.execFile)("niri", ["validate", "--config", file]); diff --git a/apps/desktop/src/snapShot/CaptureShortcutConfig.ts b/apps/desktop/src/snapShot/CaptureShortcutConfig.ts index edc4c300c101..8b86b416cdba 100644 --- a/apps/desktop/src/snapShot/CaptureShortcutConfig.ts +++ b/apps/desktop/src/snapShot/CaptureShortcutConfig.ts @@ -2,7 +2,6 @@ import * as NodeFSP from "node:fs/promises"; import * as NodeFS from "node:fs"; import * as NodePath from "node:path"; -import * as NodeOS from "node:os"; import * as NodeCrypto from "node:crypto"; import * as NodeChildProcess from "node:child_process"; import * as NodeUtil from "node:util"; @@ -39,7 +38,7 @@ const decodeHyprlandBindings = Schema.decodeUnknownSync( ), ); -export function niriCaptureConfigPath(env = process.env, home = NodeOS.homedir()) { +export function niriCaptureConfigPath(home: string, env = process.env) { return ( env.NIRI_CONFIG || NodePath.join(env.XDG_CONFIG_HOME || NodePath.join(home, ".config"), "niri", "config.kdl") @@ -99,8 +98,10 @@ export class CaptureShortcutConfig { | { preview: DesktopCaptureConfigPreview; target: Target; files: Snapshot[]; missing: string[] } | undefined; private applying = false; + private readonly home: string; private readonly tools: typeof defaultTools; - constructor(tools = defaultTools) { + constructor(home: string, tools = defaultTools) { + this.home = home; this.tools = tools; } @@ -166,7 +167,7 @@ export class CaptureShortcutConfig { if (include.path.includes("$") || /[*?[\]]/.test(include.path)) throw new Error("This config uses a dynamic include. Use manual setup in Advanced."); const path = include.path.startsWith("~/") - ? NodePath.join(NodeOS.homedir(), include.path.slice(2)) + ? NodePath.join(this.home, include.path.slice(2)) : NodePath.resolve(NodePath.dirname(file.path), include.path); let child: Snapshot; try { diff --git a/apps/desktop/src/snapShot/DesktopSnapShot.ts b/apps/desktop/src/snapShot/DesktopSnapShot.ts index f1c1c2a4908f..5718676f3b7a 100644 --- a/apps/desktop/src/snapShot/DesktopSnapShot.ts +++ b/apps/desktop/src/snapShot/DesktopSnapShot.ts @@ -88,6 +88,7 @@ import { snapShotShortcutRegistrationFailureMessage, snapShotShortcutSystemConflict, } from "./snapShot.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const MAX_CAPTURE_WIDTH = 2_560; const MAX_CAPTURE_HEIGHT = 1_600; @@ -727,6 +728,7 @@ export const make = Effect.gen(function* () { const runPromise = Effect.runPromiseWith(context); const captureDirectory = path.join(environment.stateDir, "snap-shots"); const linuxAppId = environment.linuxDesktopEntryName.replace(/\.desktop$/, ""); + const home = yield* HostProcessHomeDirectory; let shortcutVerified = false; const gnomeSetupPaths = { bundle: environment.isPackaged @@ -757,7 +759,7 @@ export const make = Effect.gen(function* () { dataHome: path.dirname(environment.linuxApplicationsDir), }; const shiftShortcutWorkerPath = path.join(__dirname, "snapShot", "GlobalShiftShortcutWorker.cjs"); - const shortcutConfig = new CaptureShortcutConfig(); + const shortcutConfig = new CaptureShortcutConfig(home); const accessibilityWorkerPath = path.join( __dirname, "snapShot", @@ -1184,7 +1186,7 @@ export const make = Effect.gen(function* () { shortcut, shortcutRegistered: false, shortcutBinding: niriCaptureBinding(linuxAppId), - shortcutConfigPath: niriCaptureConfigPath(), + shortcutConfigPath: niriCaptureConfigPath(home), shortcutActionRegistered: registered, shortcutMessage: registered ? "Set up the shortcut to add it to your Niri config." @@ -1474,8 +1476,8 @@ export const make = Effect.gen(function* () { const configPath = selectedPath ?? (desktop === "niri" - ? niriCaptureConfigPath() - : (await hyprlandCaptureShortcut(linuxAppId)).shortcutConfigPath); + ? niriCaptureConfigPath(home) + : (await hyprlandCaptureShortcut(linuxAppId, home)).shortcutConfigPath); return shortcutConfig.preview({ desktop, path: configPath, appId: linuxAppId }, request); }, catch: (cause) => @@ -1593,7 +1595,7 @@ export const make = Effect.gen(function* () { : undefined; const hyprlandShortcut = state.linuxBackend === "hyprland" - ? yield* Effect.promise(() => hyprlandCaptureShortcut(linuxAppId)) + ? yield* Effect.promise(() => hyprlandCaptureShortcut(linuxAppId, home)) : undefined; return { ...state, @@ -1609,7 +1611,7 @@ export const make = Effect.gen(function* () { : {}), ...hyprlandShortcut, ...(state.linuxBackend === "niri" - ? { shortcutConfigPath: niriCaptureConfigPath() } + ? { shortcutConfigPath: niriCaptureConfigPath(home) } : {}), ...(kdeHelper ? { diff --git a/apps/desktop/src/snapShot/HyprlandSnapShot.test.ts b/apps/desktop/src/snapShot/HyprlandSnapShot.test.ts index 3129d5bab7fd..fe55319ecea4 100644 --- a/apps/desktop/src/snapShot/HyprlandSnapShot.test.ts +++ b/apps/desktop/src/snapShot/HyprlandSnapShot.test.ts @@ -121,7 +121,7 @@ it.each([true, false])("uses the active config syntax and user bindings (Lua: %s NodePath.join(config, lua ? "bindings.lua" : "bindings.conf"), "custom bindings", ); - const result = await hyprlandCaptureShortcut("com.t3tools.T3Code", directory); + const result = await hyprlandCaptureShortcut("com.t3tools.T3Code", "/home/test", directory); expect(result.shortcutConfigPath).toBe( NodePath.join(config, lua ? "bindings.lua" : "bindings.conf"), ); diff --git a/apps/desktop/src/snapShot/HyprlandSnapShot.ts b/apps/desktop/src/snapShot/HyprlandSnapShot.ts index 2fc90a410a37..4dc57e44448d 100644 --- a/apps/desktop/src/snapShot/HyprlandSnapShot.ts +++ b/apps/desktop/src/snapShot/HyprlandSnapShot.ts @@ -29,7 +29,8 @@ function hyprlandCaptureBinding(appId: string, lua: boolean): string { /** Omarchy owns its defaults; instructions always point at a user-owned config. */ export async function hyprlandCaptureShortcut( appId: string, - configHome = process.env.XDG_CONFIG_HOME || NodePath.join(NodeOS.homedir(), ".config"), + home: string, + configHome = process.env.XDG_CONFIG_HOME || NodePath.join(home, ".config"), ) { const directory = NodePath.join(configHome, "hypr"); const exists = async (name: string) => diff --git a/apps/server/src/assets/AssetAccess.test.ts b/apps/server/src/assets/AssetAccess.test.ts index 61197efb6a50..6b6d4b2b95da 100644 --- a/apps/server/src/assets/AssetAccess.test.ts +++ b/apps/server/src/assets/AssetAccess.test.ts @@ -2,7 +2,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform"; import * as NodeFSP from "node:fs/promises"; -import * as NodeOS from "node:os"; import { AssetAccessError, AssetPreviewTypeValidationError, @@ -37,17 +36,13 @@ import { openMediaFile } from "./MediaFile.ts"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; import * as GitHubCredentials from "@t3tools/source-control-github/server/GitHubCredentials"; import { githubMediaResponse } from "./GitHubMediaFetch.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; vi.mock("node:fs/promises", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, open: vi.fn(actual.open), realpath: vi.fn(actual.realpath) }; }); -vi.mock("node:os", async (importOriginal) => { - const actual = await importOriginal(); - return { ...actual, homedir: vi.fn(actual.homedir) }; -}); - const layerConfig = ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3-asset-access-test-", }); @@ -299,8 +294,7 @@ describe("AssetAccess", () => { yield* fs.makeDirectory(path.dirname(filePath), { recursive: true }); yield* fs.writeFileString(filePath, "recording bytes"); const canonicalFile = yield* fs.realPath(filePath); - const homeSpy = vi.mocked(NodeOS.homedir).mockReturnValue(home); - try { + yield* Effect.gen(function* () { for (const workspaceRoot of [ path.join(home, "project"), path.join(directory, "srv", "project"), @@ -332,9 +326,7 @@ describe("AssetAccess", () => { expect(yield* Effect.promise(() => response.text())).toBe("recording bytes"); } } - } finally { - homeSpy.mockRestore(); - } + }).pipe(Effect.provideService(HostProcessHomeDirectory, home)); }).pipe(Effect.provide(layerTest)), ); diff --git a/apps/server/src/assets/AssetAccess.ts b/apps/server/src/assets/AssetAccess.ts index c9fbc064168c..d2de3e555736 100644 --- a/apps/server/src/assets/AssetAccess.ts +++ b/apps/server/src/assets/AssetAccess.ts @@ -58,6 +58,7 @@ import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts"; import * as WorkspacePaths from "../workspace/WorkspacePaths.ts"; import * as NativeAppIconResolver from "./NativeAppIconResolver.ts"; import { openMediaFile, readMediaFileHeader, type OpenMediaFile } from "./MediaFile.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; export const ASSET_ROUTE_PREFIX = "/api/assets"; @@ -469,7 +470,7 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i switch (input.resource._tag) { case "media-file": { - let requestedPath = expandHomePath(input.resource.path); + let requestedPath = expandHomePath(input.resource.path, yield* HostProcessHomeDirectory); if (!path.isAbsolute(requestedPath)) { if (!input.workspaceRoot) { return yield* new AssetWorkspaceContextNotFoundError({ resource: input.resource }); diff --git a/apps/server/src/cli/app.test.ts b/apps/server/src/cli/app.test.ts index 4bde751dcf41..0c35ab9f6941 100644 --- a/apps/server/src/cli/app.test.ts +++ b/apps/server/src/cli/app.test.ts @@ -9,6 +9,7 @@ import { it } from "@effect/vitest"; import type { DesktopAppActivationRequest } from "@t3tools/contracts"; import { resolveDesktopAppControlAddress } from "@t3tools/shared/desktopAppControl"; import { + HostProcessHomeDirectory, HostProcessPlatform, HostProcessUserId, HostProcessWorkingDirectory, @@ -19,18 +20,11 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; import { Command } from "effect/cli"; -import { afterEach, describe, expect, vi } from "vite-plus/test"; +import { describe, expect } from "vite-plus/test"; import { makeCli } from "../binCli.ts"; import { PersistedServerRuntimeState } from "../serverRuntimeState.ts"; -vi.mock("node:os", async (importOriginal) => { - const os = await importOriginal(); - return { ...os, homedir: vi.fn(os.homedir) }; -}); - -afterEach(() => vi.mocked(NodeOS.homedir).mockReset()); - const encodeRuntimeState = Schema.encodeEffect(Schema.fromJsonString(PersistedServerRuntimeState)); const runCli = (args: ReadonlyArray, env: Record = {}) => @@ -296,7 +290,6 @@ describe("t3 app", () => { it.effect("prefers the installed desktop app when a dev desktop is also running", () => withTempDirectory("t3-app-preferred-test-", (root) => Effect.gen(function* () { - vi.mocked(NodeOS.homedir).mockReturnValue(root); const baseDir = NodePath.join(root, ".t3"); const desktop = yield* fakeDesktop({ baseDir }); const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" }); @@ -305,14 +298,13 @@ describe("t3 app", () => { expect(desktop.received).toHaveLength(1); expect(development.received).toHaveLength(0); - }).pipe(Effect.scoped), + }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped), ), ); it.effect("finds the dev desktop when the default desktop socket is absent", () => withTempDirectory("t3-app-dev-test-", (root) => Effect.gen(function* () { - vi.mocked(NodeOS.homedir).mockReturnValue(root); const baseDir = NodePath.join(root, ".t3"); const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" }); @@ -321,14 +313,13 @@ describe("t3 app", () => { expect(development.received).toHaveLength(2); expect(yield* pathExists(baseDir)).toBe(false); - }).pipe(Effect.scoped), + }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped), ), ); it.effect("never searches a dev state directory for an explicit T3 home", () => withTempDirectory("t3-app-explicit-test-", (root) => Effect.gen(function* () { - vi.mocked(NodeOS.homedir).mockReturnValue(root); const baseDir = NodePath.join(root, ".t3"); const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" }); @@ -338,7 +329,7 @@ describe("t3 app", () => { expect(flagError).toMatchObject({ _tag: "DesktopAppUnreachableError" }); expect(envError).toMatchObject({ _tag: "DesktopAppUnreachableError" }); expect(development.received).toHaveLength(0); - }).pipe(Effect.scoped), + }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped), ), ); @@ -347,7 +338,6 @@ describe("t3 app", () => { (responseKind) => withTempDirectory("t3-app-response-test-", (root) => Effect.gen(function* () { - vi.mocked(NodeOS.homedir).mockReturnValue(root); const baseDir = NodePath.join(root, ".t3"); const desktop = yield* fakeDesktop({ baseDir, @@ -387,7 +377,7 @@ describe("t3 app", () => { cause: { message: "The desktop app response is invalid." }, }); } - }).pipe(Effect.scoped), + }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped), ), ); }); diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index c7b1fb0d5819..33db150ec689 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -31,7 +31,7 @@ import type { WebSearchOutput, } from "@anthropic-ai/claude-agent-sdk/sdk-tools"; import { parseCliArgs } from "@t3tools/shared/cliArgs"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; import { applyClaudePromptEffortPrefix } from "@t3tools/shared/model"; import { CLAUDE_RESUME_COMPACTION_NEVER_ANSWER, @@ -8181,12 +8181,12 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")( const queryRunner = yield* ClaudeAgentSdkQueryRunner; const serverConfig = yield* ServerConfig.ServerConfig; const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; - const baseEnvironment = mergeProviderInstanceEnvironment(environment, hostEnvironment); + const baseEnvironment = yield* mergeProviderInstanceEnvironment(environment, hostEnvironment); const claudeEnvironment = yield* makeClaudeEnvironment(config, baseEnvironment); const path = yield* Path.Path; const crypto = yield* Crypto.Crypto; const binaryPath = yield* resolveClaudeSdkExecutablePath( - expandHomePath(config.binaryPath), + expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory), claudeEnvironment, ); return yield* makeClaudeAdapterV2({ diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index dbdbab32a74d..69eb0aa7f38e 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -30,7 +30,7 @@ import { type ProviderSetupError, } from "@t3tools/contracts"; import { SKILL_MENTION_PATTERN } from "@t3tools/shared/composerInlineTokens"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; import { dynamicToolTitle } from "@t3tools/shared/toolActivity"; import { getModelSelectionStringOptionValue, modelSelectionsEqual } from "@t3tools/shared/model"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; @@ -1582,14 +1582,14 @@ export const createCodexAdapterV2 = ( const settings = { ...config, enabled, - binaryPath: expandHomePath(config.binaryPath), + binaryPath: expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory), homePath: homeLayout.effectiveHomePath ?? "", } satisfies CodexSettings; return yield* makeCodexAdapterV2({ instanceId, settings, - environment: mergeProviderInstanceEnvironment(environment, hostEnvironment), + environment: yield* mergeProviderInstanceEnvironment(environment, hostEnvironment), clientFactory, crypto, fileSystem, diff --git a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts index 732bc6904f40..bdfc883aeb93 100644 --- a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts @@ -94,9 +94,12 @@ describe("acpSubagentStatusBlocksTurnSettlement", () => { describe("GrokAdapterV2 capabilities", () => { it("preserves Grok's rate-limit stop and distinguishes other prompt failures", () => { - const flavor = makeGrokAcpAdapterFlavor({ - makeRuntime: () => Effect.never, - } as unknown as Parameters[0]); + const flavor = makeGrokAcpAdapterFlavor( + { + makeRuntime: () => Effect.never, + } as unknown as Parameters[0], + "/home/ada", + ); const limit = flavor.promptFailure?.( new EffectAcpErrors.AcpRequestError({ code: xAiRateLimitedErrorCode, @@ -122,9 +125,12 @@ describe("GrokAdapterV2 capabilities", () => { }); it("wires hard Stop teardown but soft non-Stop interrupts in the constructor flavor", () => { - const flavor = makeGrokAcpAdapterFlavor({ - makeRuntime: () => Effect.never, - } as unknown as Parameters[0]); + const flavor = makeGrokAcpAdapterFlavor( + { + makeRuntime: () => Effect.never, + } as unknown as Parameters[0], + "/home/ada", + ); assert.isFalse(flavor.interruptPromptOnCancel); // User Stop (requestRuntimeRestart) keeps the hard process-group kill and @@ -139,9 +145,12 @@ describe("GrokAdapterV2 capabilities", () => { }); it("terminalizes only foreground tools under the actual Grok flavor", () => { - const flavor = makeGrokAcpAdapterFlavor({ - makeRuntime: () => Effect.never, - } as unknown as Parameters[0]); + const flavor = makeGrokAcpAdapterFlavor( + { + makeRuntime: () => Effect.never, + } as unknown as Parameters[0], + "/home/ada", + ); const foreground = { toolCallId: "foreground-1", title: "Terminal", @@ -284,9 +293,12 @@ describe("ACP permission policy", () => { }); describe("Grok permission prompts", () => { - const disposition = makeGrokAcpAdapterFlavor({ - makeRuntime: () => Effect.never, - } as unknown as Parameters[0]).permissionDisposition; + const disposition = makeGrokAcpAdapterFlavor( + { + makeRuntime: () => Effect.never, + } as unknown as Parameters[0], + "/home/ada", + ).permissionDisposition; // grok_auto_blocked_command replays Auto end to end. When an explicit policy // launches Grok asking instead, T3's policy still answers its prompts. diff --git a/apps/server/src/orchestration-v2/workflowScriptQuery.ts b/apps/server/src/orchestration-v2/workflowScriptQuery.ts index 184abc6147ce..a4aa736c669b 100644 --- a/apps/server/src/orchestration-v2/workflowScriptQuery.ts +++ b/apps/server/src/orchestration-v2/workflowScriptQuery.ts @@ -14,16 +14,16 @@ * never trusted beyond these checks. */ import * as NodeFSP from "node:fs/promises"; -import * as NodeOS from "node:os"; import * as NodePath from "node:path"; import { OrchestrationGetWorkflowScriptError } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const SCRIPT_BYTE_CAP = 256 * 1024; -function scriptsRoot(): string { - return NodePath.join(NodeOS.homedir(), ".claude", "projects"); +function scriptsRoot(home: string): string { + return NodePath.join(home, ".claude", "projects"); } export const readWorkflowScript = Effect.fn("orchestration.readWorkflowScript")(function* (input: { @@ -38,8 +38,9 @@ export const readWorkflowScript = Effect.fn("orchestration.readWorkflowScript")( }); } + const home = yield* HostProcessHomeDirectory; const root = yield* Effect.tryPromise({ - try: () => NodeFSP.realpath(scriptsRoot()), + try: () => NodeFSP.realpath(scriptsRoot(home)), catch: (cause) => new OrchestrationGetWorkflowScriptError({ reason: "root-unavailable", diff --git a/apps/server/src/os-jank.ts b/apps/server/src/os-jank.ts index 18ddbc66c0c8..25c7dfdcb973 100644 --- a/apps/server/src/os-jank.ts +++ b/apps/server/src/os-jank.ts @@ -1,4 +1,8 @@ -import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { + HostProcessEnvironment, + HostProcessHomeDirectory, + HostProcessPlatform, +} from "@t3tools/shared/hostProcess"; import { listLoginShellCandidates, mergePathEntries, @@ -93,11 +97,12 @@ export const fixPath = Effect.fn("fixPath")(function* (): Effect.fn.Return< export const expandHomePath = Effect.fn(function* (input: string) { const { join } = yield* Path.Path; + const home = yield* HostProcessHomeDirectory; if (input === "~") { - return NodeOS.homedir(); + return home; } if (input.startsWith("~/") || input.startsWith("~\\")) { - return join(NodeOS.homedir(), input.slice(2)); + return join(home, input.slice(2)); } return input; }); @@ -105,7 +110,7 @@ export const expandHomePath = Effect.fn(function* (input: string) { export const resolveBaseDir = Effect.fn(function* (raw: string | undefined) { const { join, resolve } = yield* Path.Path; if (!raw || raw.trim().length === 0) { - return join(NodeOS.homedir(), ".t3"); + return join(yield* HostProcessHomeDirectory, ".t3"); } return resolve(yield* expandHomePath(raw.trim())); }); diff --git a/apps/server/src/project/AgentSessionScanner.ts b/apps/server/src/project/AgentSessionScanner.ts index 0fe74152f1c3..5177a948d945 100644 --- a/apps/server/src/project/AgentSessionScanner.ts +++ b/apps/server/src/project/AgentSessionScanner.ts @@ -44,7 +44,11 @@ import { parseGitHubRepositoryNameWithOwnerFromRemoteUrl, parseOriginUrlFromGitConfig, } from "@t3tools/shared/git"; -import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { + HostProcessEnvironment, + HostProcessHomeDirectory, + HostProcessPlatform, +} from "@t3tools/shared/hostProcess"; import { normalizeProjectPathForComparison } from "@t3tools/shared/path"; import * as ServerConfig from "../config.ts"; @@ -631,7 +635,7 @@ export const make = Effect.gen(function* () { // must case fold. const foldWorktreeCase = (yield* HostProcessPlatform) === "win32"; const hostEnvironment = yield* HostProcessEnvironment; - const homeDir = NodeOS.homedir(); + const homeDir = yield* HostProcessHomeDirectory; // `/private/tmp` is what macOS reports for sessions started in `/tmp`. const excludedProjectRoots = new Set( [homeDir, NodeOS.tmpdir(), "/tmp", "/private/tmp"].map((directory) => @@ -912,13 +916,13 @@ export const make = Effect.gen(function* () { const resolveClaudeConfigDir = (homePath: string, environmentHome?: string): string => { const configured = homePath.trim(); if (configured.length > 0) { - return path.resolve(expandHomePath(configured)); + return path.resolve(expandHomePath(configured, homeDir)); } const fromEnvironment = environmentHome?.trim() ?? ""; if (fromEnvironment.length > 0) { - return path.resolve(expandHomePath(fromEnvironment)); + return path.resolve(expandHomePath(fromEnvironment, homeDir)); } - return path.join(NodeOS.homedir(), ".claude"); + return path.join(homeDir, ".claude"); }; const discoverClaudeTranscripts = Effect.fn("AgentSessionScanner.discoverClaudeTranscripts")( @@ -1217,7 +1221,7 @@ export const make = Effect.gen(function* () { const gitIdentities = new Map(); for (const candidate of raw) { - const expanded = expandHomePath(candidate.cwd.trim()); + const expanded = expandHomePath(candidate.cwd.trim(), homeDir); if (!path.isAbsolute(expanded)) continue; const resolved = path.resolve(expanded); if (isExcludedProjectPath(resolved)) continue; @@ -1281,7 +1285,7 @@ export const make = Effect.gen(function* () { ); const importedProjectsByRoot = new Map(); for (const project of importedProjects) { - const projectRoot = path.resolve(expandHomePath(project.workspaceRoot)); + const projectRoot = path.resolve(expandHomePath(project.workspaceRoot, homeDir)); importedProjectsByRoot.set(normalizeProjectPathForComparison(projectRoot), project); importedProjectsByRoot.set(yield* directoryIdentity(projectRoot), project); } @@ -1328,7 +1332,7 @@ export const make = Effect.gen(function* () { workspaceRoot: string, completedSources: ReadonlyArray, ) { - const root = path.resolve(expandHomePath(workspaceRoot)); + const root = path.resolve(expandHomePath(workspaceRoot, homeDir)); const realRoot = yield* fileSystem.realPath(root).pipe(Effect.orElseSucceed(() => root)); if (isExcludedProjectPath(root) || isExcludedProjectPath(realRoot)) return Stream.empty; const rootIdentity = yield* directoryIdentity(root); @@ -1343,7 +1347,7 @@ export const make = Effect.gen(function* () { readonly transcript: RawCandidate["transcripts"][number] & { readonly mtimeMs: number }; }> = []; for (const candidate of candidates) { - const expanded = expandHomePath(candidate.cwd.trim()); + const expanded = expandHomePath(candidate.cwd.trim(), homeDir); if (!path.isAbsolute(expanded)) continue; const resolved = path.resolve(expanded); if ((yield* directoryIdentity(resolved)) !== rootIdentity) continue; @@ -1439,7 +1443,7 @@ export const make = Effect.gen(function* () { if (snapshotCwd === null) { return Option.some({ _tag: "Skipped" }); } - const expandedCwd = expandHomePath(snapshotCwd.trim()); + const expandedCwd = expandHomePath(snapshotCwd.trim(), homeDir); if ( !path.isAbsolute(expandedCwd) || (yield* directoryIdentity(path.resolve(expandedCwd))) !== rootIdentity diff --git a/apps/server/src/provider/CodexProvider.ts b/apps/server/src/provider/CodexProvider.ts index 913a1983a107..a8895e21ae32 100644 --- a/apps/server/src/provider/CodexProvider.ts +++ b/apps/server/src/provider/CodexProvider.ts @@ -47,6 +47,7 @@ import { type CodexRateLimitSnapshot, type CodexResetCreditsSummary, } from "./codexUsageLimits.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; import packageJson from "../../package.json" with { type: "json" }; const isCodexAppServerSpawnError = Schema.is(CodexErrors.CodexAppServerSpawnError); const RATE_LIMITS_PROBE_TIMEOUT_MS = 3_000; @@ -374,7 +375,9 @@ export const withCodexAppServerClient = Effect.fn("withCodexAppServerClient")(fu // so `CODEX_HOME=~/.codex_work` would reach codex verbatim and trip // "CODEX_HOME points to '~/.codex_work', but that path does not exist". // Expand here for parity with `CodexTextGeneration`. - const resolvedHomePath = input.homePath ? expandHomePath(input.homePath) : undefined; + const resolvedHomePath = input.homePath + ? expandHomePath(input.homePath, yield* HostProcessHomeDirectory) + : undefined; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const environment = { ...input.environment, diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.ts b/apps/server/src/provider/Drivers/AntigravityDriver.ts index c0812af2a6df..cdf938dbb4d0 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.ts @@ -1,7 +1,7 @@ import { withAgentDeviceEnvironment } from "@t3tools/provider-core/server/mcpSession"; import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; import { AntigravitySettings, ProviderDriverKind, ProviderSetupError } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime"; import { NodeRuntimeUnavailableError, @@ -113,8 +113,12 @@ export const AntigravityDriver: ProviderDriver< gcpLocation: settings.gcpLocation, }; const authConfigIssue = antigravityAuthConfigIssue(auth); - const processEnvironment = mergeProviderInstanceEnvironment(environment); - const userHome = resolveAntigravityUserHome(yield* HostProcessPlatform, processEnvironment); + const processEnvironment = yield* mergeProviderInstanceEnvironment(environment); + const userHome = resolveAntigravityUserHome( + yield* HostProcessPlatform, + processEnvironment, + yield* HostProcessHomeDirectory, + ); const directories = yield* resolveAntigravityInstanceDirectories( host.paths.stateDir, instanceId, diff --git a/apps/server/src/provider/Drivers/AntigravitySkills.test.ts b/apps/server/src/provider/Drivers/AntigravitySkills.test.ts index 180ef9968fac..f5bc8ec3f4d3 100644 --- a/apps/server/src/provider/Drivers/AntigravitySkills.test.ts +++ b/apps/server/src/provider/Drivers/AntigravitySkills.test.ts @@ -344,17 +344,35 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => { it("resolves the home the agent expands ~ against", () => { assert.equal( - resolveAntigravityUserHome("linux", { HOME: "/home/user", USERPROFILE: "C:\\Users\\user" }), + resolveAntigravityUserHome( + "linux", + { HOME: "/home/user", USERPROFILE: "C:\\Users\\user" }, + "/home/fallback", + ), "/home/user", ); assert.equal( - resolveAntigravityUserHome("win32", { HOME: "/home/user", USERPROFILE: "C:\\Users\\user" }), + resolveAntigravityUserHome( + "win32", + { HOME: "/home/user", USERPROFILE: "C:\\Users\\user" }, + "/home/fallback", + ), "C:\\Users\\user", ); assert.equal( - resolveAntigravityUserHome("win32", { HOMEDRIVE: "D:", HOMEPATH: "\\Users\\alice" }), + resolveAntigravityUserHome( + "win32", + { HOMEDRIVE: "D:", HOMEPATH: "\\Users\\alice" }, + "/home/fallback", + ), "D:\\Users\\alice", ); - assert.equal(resolveAntigravityUserHome("darwin", { HOME: "/Users/a b " }), "/Users/a b "); - assert.equal(resolveAntigravityUserHome("darwin", { HOME: "" }).length > 0, true); + assert.equal( + resolveAntigravityUserHome("darwin", { HOME: "/Users/a b " }, "/home/fallback"), + "/Users/a b ", + ); + assert.equal( + resolveAntigravityUserHome("darwin", { HOME: "" }, "/home/fallback"), + "/home/fallback", + ); }); diff --git a/apps/server/src/provider/Drivers/AntigravitySkills.ts b/apps/server/src/provider/Drivers/AntigravitySkills.ts index b11396467582..1bc4e370013f 100644 --- a/apps/server/src/provider/Drivers/AntigravitySkills.ts +++ b/apps/server/src/provider/Drivers/AntigravitySkills.ts @@ -1,5 +1,3 @@ -import * as NodeOS from "node:os"; - import type { ServerProviderSkill } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -18,15 +16,16 @@ import { parse as parseYamlDocument } from "yaml"; export function resolveAntigravityUserHome( platform: NodeJS.Platform, environment: NodeJS.ProcessEnv, + home: string, ): string { if (platform === "win32") { if (environment.USERPROFILE) return environment.USERPROFILE; if (environment.HOMEDRIVE && environment.HOMEPATH) { return `${environment.HOMEDRIVE}${environment.HOMEPATH}`; } - return NodeOS.homedir(); + return home; } - return environment.HOME || NodeOS.homedir(); + return environment.HOME || home; } /** diff --git a/apps/server/src/provider/Drivers/AntigravityUsage.ts b/apps/server/src/provider/Drivers/AntigravityUsage.ts index 7b629c857c9a..d99539b555c1 100644 --- a/apps/server/src/provider/Drivers/AntigravityUsage.ts +++ b/apps/server/src/provider/Drivers/AntigravityUsage.ts @@ -8,7 +8,6 @@ * * @module provider/Drivers/AntigravityUsage */ -import * as NodeOS from "node:os"; import { type AntigravitySettings, UsageReadError } from "@t3tools/contracts"; import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; @@ -18,7 +17,7 @@ import type { ProviderUsageReader, ProviderUsageScan, } from "@t3tools/provider-core/server/usage"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; @@ -50,7 +49,7 @@ const make = Effect.gen(function* () { /** `ANTIGRAVITY_DATA_DIR` (comma-separated) or the defaults, canonicalized. */ const dataRoots = Effect.gen(function* () { - const home = NodeOS.homedir(); + const home = yield* HostProcessHomeDirectory; const configured = hostEnvironment["ANTIGRAVITY_DATA_DIR"] ?.split(",") .map((value) => value.trim()) @@ -63,7 +62,7 @@ const make = Effect.gen(function* () { ]; const canonical = new Set(); for (const root of configured?.length ? configured : defaults) { - const resolved = path.resolve(expandHomePath(root)); + const resolved = path.resolve(expandHomePath(root, home)); canonical.add( yield* fileSystem.realPath(resolved).pipe(Effect.orElseSucceed(() => resolved)), ); diff --git a/apps/server/src/provider/Drivers/ClaudeDriver.ts b/apps/server/src/provider/Drivers/ClaudeDriver.ts index e9f37580215b..9a43be65aac4 100644 --- a/apps/server/src/provider/Drivers/ClaudeDriver.ts +++ b/apps/server/src/provider/Drivers/ClaudeDriver.ts @@ -71,6 +71,7 @@ import { makeClaudeContinuationGroupKey, resolveClaudeHomePath, } from "./ClaudeHome.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings); const DRIVER_KIND = ProviderDriverKind.make("claudeAgent"); @@ -130,7 +131,7 @@ export const ClaudeDriver: ProviderDriver { const path = yield* Path.Path; + const home = yield* HostProcessHomeDirectory; const homePath = config.homePath.trim(); if (homePath.length > 0) { - return path.resolve(expandHomePath(homePath)); + return path.resolve(expandHomePath(homePath, home)); } // Inherited env vars are not shell-expanded, so a literal `~` stays literal. const inherited = environment?.CLAUDE_CONFIG_DIR?.trim() ?? ""; if (inherited.length > 0) { return path.resolve(inherited); } - return path.resolve(path.join(NodeOS.homedir(), ".claude")); + return path.resolve(path.join(home, ".claude")); }); export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function* ( diff --git a/apps/server/src/provider/Drivers/ClaudeSkills.ts b/apps/server/src/provider/Drivers/ClaudeSkills.ts index 3ef7d925e031..3082f53b0280 100644 --- a/apps/server/src/provider/Drivers/ClaudeSkills.ts +++ b/apps/server/src/provider/Drivers/ClaudeSkills.ts @@ -13,14 +13,13 @@ * * @module provider/Drivers/ClaudeSkills */ -import * as NodeOS from "node:os"; import type { ClaudeSettings, ServerProviderSkill } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { fromLenientJson } from "@t3tools/shared/schemaJson"; import { parse as parseYamlDocument } from "yaml"; @@ -296,9 +295,10 @@ const resolveClaudeConfigDirPath = Effect.fn("resolveClaudeConfigDirPath")(funct cwd?: string, ): Effect.fn.Return { const path = yield* Path.Path; + const home = yield* HostProcessHomeDirectory; const homePath = config.homePath.trim(); if (homePath.length > 0) { - return path.resolve(expandHomePath(homePath)); + return path.resolve(expandHomePath(homePath, home)); } // No tilde expansion here: the spawned CLI receives this env var verbatim // (env vars are never shell-expanded), so a literal `~` must stay literal @@ -309,7 +309,7 @@ const resolveClaudeConfigDirPath = Effect.fn("resolveClaudeConfigDirPath")(funct if (environmentConfigDir.length > 0) { return cwd ? path.resolve(cwd, environmentConfigDir) : path.resolve(environmentConfigDir); } - return path.join(NodeOS.homedir(), ".claude"); + return path.join(home, ".claude"); }); /** diff --git a/apps/server/src/provider/Drivers/CodexDriver.ts b/apps/server/src/provider/Drivers/CodexDriver.ts index 32ffed8905b4..e1fadbcd175e 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.ts @@ -77,6 +77,7 @@ import { codexUsageReader } from "./codexUsage.ts"; import * as CodexInstallation from "../CodexInstallation.ts"; import * as ServerSecretStore from "../../auth/ServerSecretStore.ts"; import * as ServerEnvironment from "../../environment/ServerEnvironment.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const decodeCodexSettings = Schema.decodeSync(CodexSettings); const DRIVER_KIND = ProviderDriverKind.make("codex"); @@ -154,7 +155,7 @@ export const CodexDriver: ProviderDriver 0 - ? expandHomePath(value) - : path.join(NodeOS.homedir(), ".codex"); + value && value.trim().length > 0 ? expandHomePath(value, home) : path.join(home, ".codex"); return path.resolve(expanded); } @@ -45,7 +42,8 @@ export const resolveCodexHomeLayout = Effect.fn("resolveCodexHomeLayout")(functi config: CodexSettings, ): Effect.fn.Return { const path = yield* Path.Path; - const sharedHomePath = resolveHomePath(path, config.homePath); + const home = yield* HostProcessHomeDirectory; + const sharedHomePath = resolveHomePath(path, home, config.homePath); const shadowHomePath = config.shadowHomePath.trim(); if (shadowHomePath.length === 0) { return { @@ -56,7 +54,7 @@ export const resolveCodexHomeLayout = Effect.fn("resolveCodexHomeLayout")(functi }; } - const effectiveHomePath = path.resolve(expandHomePath(shadowHomePath)); + const effectiveHomePath = path.resolve(expandHomePath(shadowHomePath, home)); return { mode: "authOverlay", sharedHomePath, diff --git a/apps/server/src/provider/Drivers/CodexManagedProvider.ts b/apps/server/src/provider/Drivers/CodexManagedProvider.ts index a7181352b288..92c461390aea 100644 --- a/apps/server/src/provider/Drivers/CodexManagedProvider.ts +++ b/apps/server/src/provider/Drivers/CodexManagedProvider.ts @@ -36,7 +36,7 @@ export const makeManagedCodexProvider = Effect.fn("makeManagedCodexProvider")(fu instanceId, enabled, config, - environment: mergeProviderInstanceEnvironment(input.environment), + environment: yield* mergeProviderInstanceEnvironment(input.environment), }); const continuationIdentity = codexContinuationIdentity(runtime.homeLayout); const stamp = withInstanceIdentity({ diff --git a/apps/server/src/provider/Drivers/claudeUsage.ts b/apps/server/src/provider/Drivers/claudeUsage.ts index 155ebc6a33ec..38df3146e097 100644 --- a/apps/server/src/provider/Drivers/claudeUsage.ts +++ b/apps/server/src/provider/Drivers/claudeUsage.ts @@ -4,7 +4,6 @@ * * @module provider/Drivers/claudeUsage */ -import * as NodeOS from "node:os"; import type { ClaudeSettings } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; @@ -18,6 +17,7 @@ import { type TranscriptUsageFormat, type UsageRecord, } from "@t3tools/provider-core/server/usage"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; /** * Parses one line of a Claude Code transcript. @@ -111,10 +111,11 @@ export const claudeUsageReader: ProviderUsageReader = // An undecodable config has no trustworthy home to read. if (config === undefined) return []; const path = yield* Path.Path; + const homeDirectory = yield* HostProcessHomeDirectory; const configured = config.homePath.trim(); const home = configured - ? expandHomePath(configured) - : environment.CLAUDE_CONFIG_DIR?.trim() || path.join(NodeOS.homedir(), ".claude"); + ? expandHomePath(configured, homeDirectory) + : environment.CLAUDE_CONFIG_DIR?.trim() || path.join(homeDirectory, ".claude"); return [{ dir: path.resolve(home, "projects") }]; }), }; diff --git a/apps/server/src/provider/ProviderInstanceRegistry.test.ts b/apps/server/src/provider/ProviderInstanceRegistry.test.ts index 36db18edb3b9..7c6b191a8b84 100644 --- a/apps/server/src/provider/ProviderInstanceRegistry.test.ts +++ b/apps/server/src/provider/ProviderInstanceRegistry.test.ts @@ -39,7 +39,11 @@ import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderL import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; import type { GrokSettings } from "@t3tools/provider-grok/settings"; import type { CursorSettings } from "@t3tools/provider-cursor/settings"; -import { HostProcessPlatform, isHostWindows } from "@t3tools/shared/hostProcess"; +import { + HostProcessHomeDirectory, + HostProcessPlatform, + isHostWindows, +} from "@t3tools/shared/hostProcess"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -51,7 +55,6 @@ import { HttpClient, HttpClientResponse } from "effect/http"; import * as BackgroundPolicy from "../background/BackgroundPolicy.ts"; import * as AntigravityInstallation from "./AntigravityInstallation.ts"; import * as ServerConfig from "../config.ts"; -import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; import * as ServerSettings from "../serverSettings.ts"; import { ClaudeDriver, type ClaudeDriverEnv } from "./Drivers/ClaudeDriver.ts"; import { CodexDriver, type CodexDriverEnv } from "./Drivers/CodexDriver.ts"; @@ -153,7 +156,7 @@ const makeTildeProviderFixtures = Effect.fn( )(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const homePath = expandHomePath("~"); + const homePath = yield* HostProcessHomeDirectory; const fixtureDir = yield* fileSystem.makeTempDirectoryScoped({ directory: homePath, prefix: ".t3-provider-path-test-", diff --git a/apps/server/src/provider/antigravityAuthSupport.ts b/apps/server/src/provider/antigravityAuthSupport.ts index c4816709a2f6..4b0e769cde1d 100644 --- a/apps/server/src/provider/antigravityAuthSupport.ts +++ b/apps/server/src/provider/antigravityAuthSupport.ts @@ -2,7 +2,7 @@ import * as NodeFSP from "node:fs/promises"; import type { AntigravityAuthMethod, ProviderInstanceId } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { resolveNodeExecutable, nodeRuntimeUnavailableMessage } from "@t3tools/shared/nodeRuntime"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; @@ -317,7 +317,12 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")( const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const platform = input.platform ?? (yield* HostProcessPlatform); const userHome = - input.userHome ?? resolveAntigravityUserHome(platform, input.baseEnv ?? process.env); + input.userHome ?? + resolveAntigravityUserHome( + platform, + input.baseEnv ?? process.env, + yield* HostProcessHomeDirectory, + ); const runtimeExecutablePath = input.runtimeExecutablePath ?? (yield* resolveNodeExecutable("Antigravity sign-in", input.baseEnv).pipe( diff --git a/apps/server/src/provider/claudeResetCredits.ts b/apps/server/src/provider/claudeResetCredits.ts index 5f8379e1c037..c37a0dfb2d8d 100644 --- a/apps/server/src/provider/claudeResetCredits.ts +++ b/apps/server/src/provider/claudeResetCredits.ts @@ -7,12 +7,11 @@ * * @module provider/claudeResetCredits */ -import * as NodeOS from "node:os"; import type { ProviderConsumeResetCreditOutcome, ServerProviderResetCredits, } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -190,10 +189,14 @@ export const readClaudeResetCredits = Effect.fn("readClaudeResetCredits")( ); /** The CLI keeps the account record beside its settings, or in the home directory by default. */ -export const claudeAccountConfigPath = (configDir: string | undefined) => - Effect.map(Path.Path, (path) => - configDir ? path.join(configDir, ".claude.json") : path.join(NodeOS.homedir(), ".claude.json"), - ); +export const claudeAccountConfigPath = Effect.fn("claudeAccountConfigPath")(function* ( + configDir: string | undefined, +) { + const path = yield* Path.Path; + return configDir + ? path.join(configDir, ".claude.json") + : path.join(yield* HostProcessHomeDirectory, ".claude.json"); +}); const CLAIM_OUTCOMES = { reset: "reset", diff --git a/apps/server/src/provider/providerInstallation.ts b/apps/server/src/provider/providerInstallation.ts index d888725b3ba9..ab7257553339 100644 --- a/apps/server/src/provider/providerInstallation.ts +++ b/apps/server/src/provider/providerInstallation.ts @@ -134,18 +134,21 @@ export const makeProviderInstallation = Effect.fn("makeProviderInstallation")(fu true, ); const entries = yield* readEntries(input.instanceId, "remove-install"); - const protectedPaths = yield* Effect.forEach(Object.values(entries), (entry) => { - if (!hasBinaryPath(entry.config) || !entry.config.binaryPath.trim()) { - return Effect.succeed([]); - } - const binaryPath = entry.config.binaryPath.trim(); - return resolveCommandPath(binaryPath, { - env: mergeProviderInstanceEnvironment(entry.environment), - }).pipe( - Effect.map((resolved) => [binaryPath, resolved]), - Effect.orElseSucceed(() => [binaryPath]), - ); - }); + const protectedPaths = yield* Effect.forEach( + Object.values(entries), + Effect.fnUntraced(function* (entry) { + if (!hasBinaryPath(entry.config) || !entry.config.binaryPath.trim()) { + return []; + } + const binaryPath = entry.config.binaryPath.trim(); + return yield* resolveCommandPath(binaryPath, { + env: yield* mergeProviderInstanceEnvironment(entry.environment), + }).pipe( + Effect.map((resolved) => [binaryPath, resolved]), + Effect.orElseSucceed(() => [binaryPath]), + ); + }), + ); yield* installation .remove(protectedPaths.flat()) .pipe(Effect.mapError(failure(input.instanceId))); diff --git a/apps/server/src/review/ReviewService.ts b/apps/server/src/review/ReviewService.ts index 7aee5847001f..ef7a0e31590f 100644 --- a/apps/server/src/review/ReviewService.ts +++ b/apps/server/src/review/ReviewService.ts @@ -20,6 +20,7 @@ import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; import * as ServerSettings from "../serverSettings.ts"; import { isFilesystemRoot, managedWorktreesDirectories } from "../worktreesDirectory.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; export class ReviewService extends Context.Service< ReviewService, @@ -73,13 +74,14 @@ export const make = Effect.gen(function* () { const worktreesDirectories = yield* settings.getSettings.pipe( Effect.orElseSucceed(() => ({ worktreesDirectory: "", previousWorktreesDirectories: [] })), ); + const home = yield* HostProcessHomeDirectory; const [candidate, workspaceRoot, worktreesRoots] = yield* Effect.all([ canonicalizePath(cwd), canonicalizePath(config.cwd), // A managed root that cannot be resolved, or resolves to a filesystem // root through a symlink, is skipped rather than failing every review. Effect.forEach( - managedWorktreesDirectories(worktreesDirectories, config.worktreesDir, path), + managedWorktreesDirectories(worktreesDirectories, config.worktreesDir, path, home), (directory) => canonicalizePath(directory).pipe(Effect.orElseSucceed(() => null)), ).pipe( Effect.map((roots) => diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.ts index 1bbca1e2b3d5..e595b4028cee 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.ts @@ -29,6 +29,7 @@ import { import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError); const isSourceControlProviderError = Schema.is(SourceControlProviderError); const isBitbucketRepositoryLocatorError = Schema.is(BitbucketApi.BitbucketRepositoryLocatorError); @@ -213,7 +214,7 @@ export const make = Effect.gen(function* () { }); } - return path.resolve(expandHomePath(trimmed)); + return path.resolve(expandHomePath(trimmed, yield* HostProcessHomeDirectory)); }, ); diff --git a/apps/server/src/storageCleanup.ts b/apps/server/src/storageCleanup.ts index d823e84e45dd..c0ad95a63c61 100644 --- a/apps/server/src/storageCleanup.ts +++ b/apps/server/src/storageCleanup.ts @@ -38,6 +38,7 @@ import * as TerminalManager from "./terminal/Manager.ts"; import { isFilesystemRoot, managedWorktreesDirectories } from "./worktreesDirectory.ts"; import * as GitVcsDriver from "./vcs/GitVcsDriver.ts"; import { withWorkspaceLease } from "./workspace/workspaceLease.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const decodeCleanupThread = Schema.decodeUnknownEffect( Schema.fromJsonString(OrchestrationV2AppThreadJson), @@ -213,6 +214,7 @@ export const make = Effect.gen(function* () { serverSettings, config.worktreesDir, path, + yield* HostProcessHomeDirectory, )) { // An unmounted drive only skips its own worktrees. const root = yield* fs.exists(directory).pipe( diff --git a/apps/server/src/telemetry/Identify.ts b/apps/server/src/telemetry/Identify.ts index 4009625d5752..a0e6c1c7ecfd 100644 --- a/apps/server/src/telemetry/Identify.ts +++ b/apps/server/src/telemetry/Identify.ts @@ -1,4 +1,3 @@ -import * as NodeOS from "node:os"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Hex from "effect/encoding/Hex"; @@ -10,6 +9,7 @@ import * as Schema from "effect/Schema"; import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite"; import * as ServerConfig from "../config.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; /** * Codex writes `tokens` only for ChatGPT logins and omits the key for API-key, @@ -311,6 +311,7 @@ export const getTelemetryIdentifierForHome = Effect.fn("getTelemetryIdentifierFo Effect.orElseSucceed(() => null), ); -export const getTelemetryIdentifier = Effect.suspend(() => - getTelemetryIdentifierForHome(NodeOS.homedir()), +export const getTelemetryIdentifier = Effect.flatMap( + HostProcessHomeDirectory, + getTelemetryIdentifierForHome, ); diff --git a/apps/server/src/terminal/Manager.ts b/apps/server/src/terminal/Manager.ts index 8aa64408993e..2a16b9d44f75 100644 --- a/apps/server/src/terminal/Manager.ts +++ b/apps/server/src/terminal/Manager.ts @@ -42,7 +42,11 @@ import { } from "@t3tools/contracts"; import { makeKeyedCoalescingWorker } from "@t3tools/shared/KeyedCoalescingWorker"; import * as KeyedLock from "@t3tools/shared/KeyedLock"; -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { + HostProcessArchitecture, + HostProcessHomeDirectory, + HostProcessPlatform, +} from "@t3tools/shared/hostProcess"; import { mergePathEntries } from "@t3tools/shared/shell"; import { acpRegistryManagedBinaryDirectories } from "@t3tools/provider-acp-registry/server"; @@ -1317,6 +1321,7 @@ function createTerminalSpawnEnv( baseEnv: NodeJS.ProcessEnv, runtimeEnv: Record | null | undefined, platform: NodeJS.Platform, + home: string, ): NodeJS.ProcessEnv { const spawnEnv: NodeJS.ProcessEnv = {}; for (const [key, value] of Object.entries(baseEnv)) { @@ -1331,7 +1336,7 @@ function createTerminalSpawnEnv( ? Object.keys(spawnEnv).find((candidate) => candidate.toLowerCase() === key.toLowerCase()) : undefined; spawnEnv[existingKey ?? key] = - key === "CODEX_HOME" || key === "CLAUDE_CONFIG_DIR" ? expandHomePath(value) : value; + key === "CODEX_HOME" || key === "CLAUDE_CONFIG_DIR" ? expandHomePath(value, home) : value; } } // An explicit empty override opts out for terminals started without a client. @@ -1408,7 +1413,7 @@ export const resolveProviderInstanceTerminalEnvironment = Effect.fn( return yield* new TerminalProviderInstanceNotFoundError({ providerInstanceId }); } - let resolved = mergeProviderInstanceEnvironment(instance.environment, input.env ?? {}); + let resolved = yield* mergeProviderInstanceEnvironment(instance.environment, input.env ?? {}); if (instance.driver === "codex") { const config = decodeCodexSettings(instance.config ?? {}); if (Option.isSome(config)) { @@ -2244,7 +2249,12 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func Effect.andThen( Effect.gen(function* () { const shellCandidates = resolveShellCandidates(shellResolver, platform, baseEnv); - const terminalEnv = createTerminalSpawnEnv(baseEnv, session.runtimeEnv, platform); + const terminalEnv = createTerminalSpawnEnv( + baseEnv, + session.runtimeEnv, + platform, + yield* HostProcessHomeDirectory, + ); // Append (never prepend) managed ACP agent install directories so // `kimi login` and friends resolve by name without shadowing any // system or user tool of the same name. diff --git a/apps/server/src/textGeneration/CodexTextGeneration.ts b/apps/server/src/textGeneration/CodexTextGeneration.ts index 0477c7fa163b..c11fed75814d 100644 --- a/apps/server/src/textGeneration/CodexTextGeneration.ts +++ b/apps/server/src/textGeneration/CodexTextGeneration.ts @@ -26,6 +26,7 @@ import { } from "@t3tools/provider-core/server/textGenerationUtils"; import { codexModelFamily, getModelSelectionStringOptionValue } from "@t3tools/shared/model"; import { getCodexServiceTierOptionValue } from "../codexModelOptions.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const CODEX_TIMEOUT_MS = 180_000; const encodeJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); @@ -214,7 +215,12 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func env: { ...effectiveEnvironment, ...(effectiveConfig.homePath - ? { CODEX_HOME: expandHomePath(effectiveConfig.homePath) } + ? { + CODEX_HOME: expandHomePath( + effectiveConfig.homePath, + yield* HostProcessHomeDirectory, + ), + } : {}), }, cwd, diff --git a/apps/server/src/usage/UsageService.ts b/apps/server/src/usage/UsageService.ts index ba65ba2887bb..2ceeb8750001 100644 --- a/apps/server/src/usage/UsageService.ts +++ b/apps/server/src/usage/UsageService.ts @@ -339,10 +339,10 @@ export const make = Effect.gen(function* () { * not decode. Disabled accounts still have history. An unconfigured default * slot runs with default config, just as it does in the provider registry. */ - const usageInstances = ( + const usageInstances = Effect.fn("UsageService.usageInstances")(function* ( driver: ProviderDriver, settings: ServerSettingsValue, - ): Array> => { + ) { const entries: Array< readonly [ProviderInstanceId, Pick, boolean] > = Object.entries(settings.providerInstances) @@ -352,13 +352,22 @@ export const make = Effect.gen(function* () { entries.push([ProviderInstanceId.make(driver.driverKind), {}, false]); } const decodeConfig = Schema.decodeUnknownOption(driver.configSchema); - return entries.map(([instanceId, instance, configured]) => ({ - instanceId, - config: Option.getOrUndefined(decodeConfig(instance.config ?? {})), - environment: mergeProviderInstanceEnvironment(instance.environment, hostEnvironment), - configured, - })); - }; + return yield* Effect.forEach( + entries, + Effect.fnUntraced(function* ([instanceId, instance, configured]) { + const instanceConfig: ProviderUsageInstance = { + instanceId, + config: Option.getOrUndefined(decodeConfig(instance.config ?? {})), + environment: yield* mergeProviderInstanceEnvironment( + instance.environment, + hostEnvironment, + ), + configured, + }; + return instanceConfig; + }), + ); + }); /** Resolves every transcript directory the usage readers point at. */ const resolveTranscriptDirs = Effect.fn("UsageService.resolveTranscriptDirs")(function* ( @@ -369,8 +378,9 @@ export const make = Effect.gen(function* () { const seen = new Set(); for (const { driver, reader } of transcriptReaders) { const { provider, format } = reader; - const directories = yield* Effect.forEach(usageInstances(driver, settings), (instance) => - reader.directories(instance), + const directories = yield* Effect.forEach( + yield* usageInstances(driver, settings), + (instance) => reader.directories(instance), ); for (const { dir: directory, fileName } of directories.flat()) { const sourceKey = provider + "\0" + directory; @@ -635,14 +645,12 @@ export const make = Effect.gen(function* () { const scans = Effect.forEach( scanReaders, ({ driver, reader }) => - reader - .scan({ - instances: usageInstances(driver, settings), - settings, - windowStartMs, - retentionCutoffMs, - awaitRefresh, - }) + usageInstances(driver, settings) + .pipe( + Effect.flatMap((instances) => + reader.scan({ instances, settings, windowStartMs, retentionCutoffMs, awaitRefresh }), + ), + ) .pipe( Effect.flatMap((sources) => Effect.forEach(sources, ({ volumeId, ...source }) => diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 002311fda2cf..b273e9eff52f 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -29,7 +29,7 @@ import { type VcsRef, } from "@t3tools/contracts"; import { dedupeRemoteBranchesWithLocalMatches, normalizeGitRemoteUrl } from "@t3tools/shared/git"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { compactTraceAttributes } from "@t3tools/shared/observability"; import { decodeJsonResult } from "@t3tools/shared/schemaJson"; import { parseT3ProjectFile } from "@t3tools/shared/t3ProjectFile"; @@ -3403,6 +3403,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* options?.worktreesDirectory ?? "", worktreesDir, path, + yield* HostProcessHomeDirectory, ); if (parentDir === null) { return yield* new GitCommandError({ diff --git a/apps/server/src/workspace/WorkspaceEntries.ts b/apps/server/src/workspace/WorkspaceEntries.ts index beac8d18a094..153360443338 100644 --- a/apps/server/src/workspace/WorkspaceEntries.ts +++ b/apps/server/src/workspace/WorkspaceEntries.ts @@ -19,7 +19,7 @@ import type { ProjectSearchEntriesInput, ProjectSearchEntriesResult, } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { isExplicitRelativePath, isWindowsAbsolutePath } from "@t3tools/shared/path"; import { normalizeSearchQuery } from "@t3tools/shared/searchRanking"; @@ -120,7 +120,7 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu } if (!isExplicitRelativePath(input.partialPath)) { - return path.resolve(expandHomePath(input.partialPath)); + return path.resolve(expandHomePath(input.partialPath, yield* HostProcessHomeDirectory)); } if (!input.cwd) { @@ -128,7 +128,10 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu partialPath: input.partialPath, }); } - return path.resolve(expandHomePath(input.cwd), input.partialPath); + return path.resolve( + expandHomePath(input.cwd, yield* HostProcessHomeDirectory), + input.partialPath, + ); }); /** @public Service construction is part of the canonical Effect module API. */ diff --git a/apps/server/src/workspace/WorkspacePaths.ts b/apps/server/src/workspace/WorkspacePaths.ts index 000a422bcdc8..748689330072 100644 --- a/apps/server/src/workspace/WorkspacePaths.ts +++ b/apps/server/src/workspace/WorkspacePaths.ts @@ -15,6 +15,7 @@ import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; export class WorkspaceRootNotExistsError extends Schema.TaggedError()( "WorkspaceRootNotExistsError", @@ -152,7 +153,9 @@ export const make = Effect.gen(function* () { const normalizeWorkspaceRoot: WorkspacePaths["Service"]["normalizeWorkspaceRoot"] = Effect.fn( "WorkspacePaths.normalizeWorkspaceRoot", )(function* (workspaceRoot, options) { - const normalizedWorkspaceRoot = path.resolve(expandHomePath(workspaceRoot.trim())); + const normalizedWorkspaceRoot = path.resolve( + expandHomePath(workspaceRoot.trim(), yield* HostProcessHomeDirectory), + ); let workspaceStat = yield* statWorkspaceRoot( workspaceRoot, normalizedWorkspaceRoot, diff --git a/apps/server/src/worktreesDirectory.ts b/apps/server/src/worktreesDirectory.ts index ee7154577458..65542b2c03bc 100644 --- a/apps/server/src/worktreesDirectory.ts +++ b/apps/server/src/worktreesDirectory.ts @@ -4,7 +4,8 @@ import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; /** * Directory new worktrees are created under: the `worktreesDirectory` - * setting, or `defaultDir` (`/worktrees`) when it is empty. Null when + * setting (`~` expands to `home`), or `defaultDir` (`/worktrees`) + * when it is empty. Null when * the setting is not an absolute path on this machine, such as `D:\worktrees` * configured for a Windows server and synced to a Linux one, or when it is a * filesystem root, which would make every path on that drive look managed. @@ -13,9 +14,10 @@ export function resolveWorktreesDirectory( setting: string, defaultDir: string, path: Path.Path, + home: string, ): string | null { if (setting === "") return defaultDir; - const expanded = expandHomePath(setting); + const expanded = expandHomePath(setting, home); if (!path.isAbsolute(expanded)) return null; const resolved = path.resolve(expanded); return isFilesystemRoot(resolved, path) ? null : resolved; @@ -34,10 +36,11 @@ export function managedWorktreesDirectories( }, defaultDir: string, path: Path.Path, + home: string, ): ReadonlyArray { const directories = new Set([defaultDir]); for (const setting of [settings.worktreesDirectory, ...settings.previousWorktreesDirectories]) { - const directory = resolveWorktreesDirectory(setting, defaultDir, path); + const directory = resolveWorktreesDirectory(setting, defaultDir, path, home); if (directory !== null) directories.add(directory); } return [...directories]; diff --git a/packages/provider-acp-registry/src/server/adapter.ts b/packages/provider-acp-registry/src/server/adapter.ts index 872eb0999802..4827ca4e4c56 100644 --- a/packages/provider-acp-registry/src/server/adapter.ts +++ b/packages/provider-acp-registry/src/server/adapter.ts @@ -278,7 +278,7 @@ export const AcpRegistryAdapterV2Driver: ProviderAdapterDriver< return yield* makeAcpRegistryAdapterV2({ instanceId: input.instanceId, settings: { ...input.config, enabled: input.enabled }, - environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment), + environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment), selfInvocation, nativeLogging: (threadId) => makeNativeLogger({ diff --git a/packages/provider-acp-registry/src/server/driver.ts b/packages/provider-acp-registry/src/server/driver.ts index bcfd83af7a39..4468aa70af39 100644 --- a/packages/provider-acp-registry/src/server/driver.ts +++ b/packages/provider-acp-registry/src/server/driver.ts @@ -497,7 +497,10 @@ export const AcpRegistryDriver: ProviderDriver(); const stderrTailRef = yield* Ref.make(""); + const homeDirectory = yield* HostProcessHomeDirectory; const stderrDrained = yield* Deferred.make(); const runtimeClosed = yield* Deferred.make(); const promptSerializationSemaphore = yield* Semaphore.make(1); @@ -1440,7 +1441,7 @@ export const make = ( Effect.ignore, Effect.andThen(Ref.get(stderrTailRef)), Effect.map((tail) => { - const stderr = sanitizeAcpStderrExcerpt(tail); + const stderr = sanitizeAcpStderrExcerpt(tail, homeDirectory); return stderr.length === 0 ? error : new EffectAcpErrors.AcpProcessExitedError({ diff --git a/packages/provider-acp/src/server/stderr.test.ts b/packages/provider-acp/src/server/stderr.test.ts index f5f704f04996..5e5c42b63cd7 100644 --- a/packages/provider-acp/src/server/stderr.test.ts +++ b/packages/provider-acp/src/server/stderr.test.ts @@ -20,6 +20,7 @@ describe("AcpStderr", () => { "Visit http://localhost:5733/pair#token=ABCDEF for pairing", "key=sk-abcdefghijklmnopqrstuv", ].join("\n"), + "/home/server", { HOME: "/Users/ada" }, ); @@ -41,6 +42,8 @@ describe("AcpStderr", () => { "Authorization: Basic dXNlcjpwYXNz", "x-api-key: ant-api-key-value", ].join("\n"), + "/Users/ada", + {}, ); expect(excerpt).toContain("[redacted]"); diff --git a/packages/provider-acp/src/server/stderr.ts b/packages/provider-acp/src/server/stderr.ts index 52f68bb4c19a..01ed491d8732 100644 --- a/packages/provider-acp/src/server/stderr.ts +++ b/packages/provider-acp/src/server/stderr.ts @@ -1,6 +1,3 @@ -// @effect-diagnostics nodeBuiltinImport:off -- The excerpt sanitizer masks the home directory, which only the Node os module can resolve. -import * as NodeOS from "node:os"; - /** Last few KiB of ACP child stderr kept for startup / exit diagnostics. */ export const ACP_STDERR_TAIL_MAX_CHARS = 4_096; @@ -19,10 +16,11 @@ export function appendAcpStderrTail(current: string, chunk: string): string { /** Bounded, redacted excerpt safe to put on user-facing adapter errors. */ export function sanitizeAcpStderrExcerpt( text: string, + homeDirectory: string, environment: NodeJS.ProcessEnv = process.env, ): string { let result = text.replaceAll("\0", ""); - const homes = [environment.HOME, environment.USERPROFILE, NodeOS.homedir()].filter( + const homes = [environment.HOME, environment.USERPROFILE, homeDirectory].filter( (value): value is string => typeof value === "string" && value.length > 1, ); for (const home of new Set(homes)) { diff --git a/packages/provider-core/src/server/instanceEnvironment.test.ts b/packages/provider-core/src/server/instanceEnvironment.test.ts index 73d2ae3c313c..e64cd31650f5 100644 --- a/packages/provider-core/src/server/instanceEnvironment.test.ts +++ b/packages/provider-core/src/server/instanceEnvironment.test.ts @@ -1,9 +1,8 @@ -import * as NodeOS from "node:os"; - import * as NodeServices from "@effect/platform-node/NodeServices"; import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Path from "effect/Path"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; import { mergeProviderInstanceEnvironment } from "./instanceEnvironment.ts"; @@ -18,7 +17,7 @@ describe("mergeProviderInstanceEnvironment", () => { CODEX_HOME: "~/.inherited-codex", CLAUDE_CONFIG_DIR: "~/.inherited-claude", }; - const environment = mergeProviderInstanceEnvironment( + const environment = yield* mergeProviderInstanceEnvironment( [ { name: "CODEX_HOME", value, sensitive: false }, { name: "CLAUDE_CONFIG_DIR", value, sensitive: false }, @@ -28,41 +27,48 @@ describe("mergeProviderInstanceEnvironment", () => { ); expect(environment).toEqual({ - CODEX_HOME: path.join(NodeOS.homedir(), tail), - CLAUDE_CONFIG_DIR: path.join(NodeOS.homedir(), tail), + CODEX_HOME: path.join("/home/ada", tail), + CLAUDE_CONFIG_DIR: path.join("/home/ada", tail), CUSTOM_VALUE: value, }); expect(baseEnv).toEqual({ CODEX_HOME: "~/.inherited-codex", CLAUDE_CONFIG_DIR: "~/.inherited-claude", }); - }).pipe(Effect.provide(NodeServices.layer)), + }).pipe( + Effect.provideService(HostProcessHomeDirectory, "/home/ada"), + Effect.provide(NodeServices.layer), + ), ); - it("leaves inherited provider homes unchanged", () => { - const baseEnv = { CODEX_HOME: "~/.codex", CLAUDE_CONFIG_DIR: "~\\.claude" }; + it.effect("leaves inherited provider homes unchanged", () => + Effect.gen(function* () { + const baseEnv = { CODEX_HOME: "~/.codex", CLAUDE_CONFIG_DIR: "~\\.claude" }; - expect( - mergeProviderInstanceEnvironment( - [{ name: "CUSTOM_VALUE", value: "~/.custom", sensitive: false }], - baseEnv, - ), - ).toEqual({ ...baseEnv, CUSTOM_VALUE: "~/.custom" }); - }); + expect( + yield* mergeProviderInstanceEnvironment( + [{ name: "CUSTOM_VALUE", value: "~/.custom", sensitive: false }], + baseEnv, + ), + ).toEqual({ ...baseEnv, CUSTOM_VALUE: "~/.custom" }); + }), + ); - it("overrides inherited environment values and preserves empty strings", () => { - expect( - mergeProviderInstanceEnvironment( - [ - { name: "OPENROUTER_API_KEY", value: "sk-or-test", sensitive: true }, - { name: "ANTHROPIC_API_KEY", value: "", sensitive: false }, - ], - { ANTHROPIC_API_KEY: "inherited", PATH: "/bin" }, - ), - ).toMatchObject({ - OPENROUTER_API_KEY: "sk-or-test", - ANTHROPIC_API_KEY: "", - PATH: "/bin", - }); - }); + it.effect("overrides inherited environment values and preserves empty strings", () => + Effect.gen(function* () { + expect( + yield* mergeProviderInstanceEnvironment( + [ + { name: "OPENROUTER_API_KEY", value: "sk-or-test", sensitive: true }, + { name: "ANTHROPIC_API_KEY", value: "", sensitive: false }, + ], + { ANTHROPIC_API_KEY: "inherited", PATH: "/bin" }, + ), + ).toMatchObject({ + OPENROUTER_API_KEY: "sk-or-test", + ANTHROPIC_API_KEY: "", + PATH: "/bin", + }); + }), + ); }); diff --git a/packages/provider-core/src/server/instanceEnvironment.ts b/packages/provider-core/src/server/instanceEnvironment.ts index ff8c9815731a..0ddb61af4b5c 100644 --- a/packages/provider-core/src/server/instanceEnvironment.ts +++ b/packages/provider-core/src/server/instanceEnvironment.ts @@ -1,22 +1,25 @@ import type { ProviderInstanceEnvironment } from "@t3tools/contracts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as Effect from "effect/Effect"; import { expandHomePath } from "./pathExpansion.ts"; -export function mergeProviderInstanceEnvironment( +export const mergeProviderInstanceEnvironment = Effect.fn(function* ( environment: ProviderInstanceEnvironment | undefined, baseEnv: NodeJS.ProcessEnv = process.env, -): NodeJS.ProcessEnv { +) { if (!environment || environment.length === 0) { return baseEnv; } + const home = yield* HostProcessHomeDirectory; const next: NodeJS.ProcessEnv = { ...baseEnv }; for (const variable of environment) { // Child processes do not apply shell expansion to environment values. next[variable.name] = variable.name === "CODEX_HOME" || variable.name === "CLAUDE_CONFIG_DIR" - ? expandHomePath(variable.value) + ? expandHomePath(variable.value, home) : variable.value; } return next; -} +}); diff --git a/packages/provider-core/src/server/pathExpansion.test.ts b/packages/provider-core/src/server/pathExpansion.test.ts index cc7c85786da6..46a9faf87e8a 100644 --- a/packages/provider-core/src/server/pathExpansion.test.ts +++ b/packages/provider-core/src/server/pathExpansion.test.ts @@ -1,34 +1,35 @@ // @effect-diagnostics nodeBuiltinImport:off -import * as NodeOS from "node:os"; import * as NodePath from "node:path"; import { describe, expect, it } from "vite-plus/test"; import { expandHomePath } from "./pathExpansion.ts"; +const home = "/home/ada"; + describe("expandHomePath", () => { it("returns an empty string unchanged", () => { - expect(expandHomePath("")).toBe(""); + expect(expandHomePath("", home)).toBe(""); }); it("returns paths without a leading tilde unchanged", () => { - expect(expandHomePath("/absolute/path")).toBe("/absolute/path"); - expect(expandHomePath("relative/path")).toBe("relative/path"); - expect(expandHomePath("some~weird~path")).toBe("some~weird~path"); + expect(expandHomePath("/absolute/path", home)).toBe("/absolute/path"); + expect(expandHomePath("relative/path", home)).toBe("relative/path"); + expect(expandHomePath("some~weird~path", home)).toBe("some~weird~path"); }); it("expands a lone tilde to the home directory", () => { - expect(expandHomePath("~")).toBe(NodeOS.homedir()); + expect(expandHomePath("~", home)).toBe(home); }); it("expands ~/ to a subpath of the home directory", () => { - expect(expandHomePath("~/.codex-work")).toBe(NodePath.join(NodeOS.homedir(), ".codex-work")); + expect(expandHomePath("~/.codex-work", home)).toBe(NodePath.join(home, ".codex-work")); }); it("expands a Windows-style ~\\ prefix", () => { - expect(expandHomePath("~\\.codex")).toBe(NodePath.join(NodeOS.homedir(), ".codex")); + expect(expandHomePath("~\\.codex", home)).toBe(NodePath.join(home, ".codex")); }); it("does not expand ~user paths", () => { - expect(expandHomePath("~alice/foo")).toBe("~alice/foo"); + expect(expandHomePath("~alice/foo", home)).toBe("~alice/foo"); }); }); diff --git a/packages/provider-core/src/server/pathExpansion.ts b/packages/provider-core/src/server/pathExpansion.ts index bacdaece0b1c..d486b2988d47 100644 --- a/packages/provider-core/src/server/pathExpansion.ts +++ b/packages/provider-core/src/server/pathExpansion.ts @@ -1,12 +1,12 @@ // @effect-diagnostics nodeBuiltinImport:off -import * as NodeOS from "node:os"; import * as NodePath from "node:path"; /** - * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to the - * current user's home directory. Spawned processes don't get shell - * expansion, so env vars like `CODEX_HOME=~/.codex-work` would be passed - * verbatim and treated as relative paths by the receiver. + * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to `home`, + * the current user's home directory (`HostProcessHomeDirectory`). Spawned + * processes don't get shell expansion, so env vars like + * `CODEX_HOME=~/.codex-work` would be passed verbatim and treated as relative + * paths by the receiver. * * Matches the behavior of the other `expandHomePath` helpers in the * workspace layers and CLI bootstrap: `~` alone and both `~/` and `~\` @@ -14,11 +14,11 @@ import * as NodePath from "node:path"; * start with `~` or is empty. Does not handle `~user` (other-user) * expansion. */ -export function expandHomePath(value: string): string { +export function expandHomePath(value: string, home: string): string { if (!value) return value; - if (value === "~") return NodeOS.homedir(); + if (value === "~") return home; if (value.startsWith("~/") || value.startsWith("~\\")) { - return NodePath.join(NodeOS.homedir(), value.slice(2)); + return NodePath.join(home, value.slice(2)); } return value; } diff --git a/packages/provider-cursor/src/server/CursorUsageAccounts.ts b/packages/provider-cursor/src/server/CursorUsageAccounts.ts index 852d746aed00..56f64104a449 100644 --- a/packages/provider-cursor/src/server/CursorUsageAccounts.ts +++ b/packages/provider-cursor/src/server/CursorUsageAccounts.ts @@ -10,12 +10,15 @@ * * @module provider-cursor/server/CursorUsageAccounts */ -import * as NodeOS from "node:os"; import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; import type { ProviderUsageScan } from "@t3tools/provider-core/server/usage"; import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite"; -import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { + HostProcessEnvironment, + HostProcessHomeDirectory, + HostProcessPlatform, +} from "@t3tools/shared/hostProcess"; import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; import * as Deferred from "effect/Deferred"; @@ -283,7 +286,7 @@ const make = Effect.gen(function* () { const scan: CursorUsageAccounts["Service"]["scan"] = Effect.fn("CursorUsageAccounts.scan")( function* ({ keychainUsageEnabled, windowStartMs, retentionCutoffMs, awaitRefresh }) { - const home = NodeOS.homedir(); + const home = yield* HostProcessHomeDirectory; const userHome = (platform === "win32" ? hostEnvironment["USERPROFILE"] : hostEnvironment["HOME"]) || home; const configHome = hostEnvironment["XDG_CONFIG_HOME"]?.trim(); diff --git a/packages/provider-cursor/src/server/adapter.ts b/packages/provider-cursor/src/server/adapter.ts index ecca0e80ac7a..fa557d6d8bc1 100644 --- a/packages/provider-cursor/src/server/adapter.ts +++ b/packages/provider-cursor/src/server/adapter.ts @@ -2627,7 +2627,7 @@ export const CursorAdapterV2Driver: ProviderAdapterDriver< ...input.config, enabled: input.enabled, }, - environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment), + environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment), }); }, (effect, input) => diff --git a/packages/provider-cursor/src/server/driver.ts b/packages/provider-cursor/src/server/driver.ts index a9fa4b31acb4..2c748bb70d5e 100644 --- a/packages/provider-cursor/src/server/driver.ts +++ b/packages/provider-cursor/src/server/driver.ts @@ -99,7 +99,7 @@ export const CursorDriver: ProviderDriver< const httpClient = yield* HttpClient.HttpClient; const keychain = yield* CursorKeychain.CursorKeychain; const sdkRunner = yield* CursorAgentSdk.CursorAgentSdkRunner; - const processEnv = mergeProviderInstanceEnvironment(environment); + const processEnv = yield* mergeProviderInstanceEnvironment(environment); const continuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, instanceId, diff --git a/packages/provider-cursor/src/server/skills.ts b/packages/provider-cursor/src/server/skills.ts index c8cd561dfea0..4c7b0bbea985 100644 --- a/packages/provider-cursor/src/server/skills.ts +++ b/packages/provider-cursor/src/server/skills.ts @@ -8,7 +8,6 @@ * * @module provider/Drivers/CursorSkills */ -import * as NodeOS from "node:os"; import type { ServerProviderSkill } from "@t3tools/contracts"; import * as ByteSize from "effect/ByteSize"; @@ -18,6 +17,7 @@ import * as Path from "effect/Path"; import * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; import { parse as parseYamlDocument } from "yaml"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/; const SKILL_MENTION_PATTERN = @@ -221,7 +221,10 @@ const inspectCursorSkills = Effect.fn("inspectCursorSkills")(function* ( environment: NodeJS.ProcessEnv = process.env, ) { const path = yield* Path.Path; - const userHome = environment.HOME?.trim() || environment.USERPROFILE?.trim() || NodeOS.homedir(); + const userHome = + environment.HOME?.trim() || + environment.USERPROFILE?.trim() || + (yield* HostProcessHomeDirectory); const rootsBelow = (base: string, scope: "user" | "project") => [ { directory: path.join(base, ".cursor", "skills"), scope }, { directory: path.join(base, ".agents", "skills"), scope }, diff --git a/packages/provider-cursor/src/server/textGeneration.ts b/packages/provider-cursor/src/server/textGeneration.ts index 59fa3dfc452d..6a6b8ae989cf 100644 --- a/packages/provider-cursor/src/server/textGeneration.ts +++ b/packages/provider-cursor/src/server/textGeneration.ts @@ -1,4 +1,3 @@ -import * as NodeOS from "node:os"; import * as FileSystem from "effect/FileSystem"; import type { AgentOptions, RunResult } from "@cursor/sdk"; @@ -13,6 +12,7 @@ import * as TextGenerationOperations from "@t3tools/provider-core/server/textGen import { cursorSdkModelSelection } from "./sdkModel.ts"; import type { CursorAuth } from "./auth.ts"; import * as CursorSdk from "./CursorSdk.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const CURSOR_TIMEOUT_MS = 180_000; @@ -90,7 +90,7 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu const apiKey = yield* resolveCursorApiKey(operation); // The SDK loads sandbox.json independently of settingSources and lets it // expand the writable paths. Its public API cannot override that policy. - if (yield* fs.exists(`${NodeOS.homedir()}/.cursor/sandbox.json`)) { + if (yield* fs.exists(`${yield* HostProcessHomeDirectory}/.cursor/sandbox.json`)) { return yield* new TextGenerationError({ operation, detail: diff --git a/packages/provider-cursor/src/server/usageLimits.ts b/packages/provider-cursor/src/server/usageLimits.ts index 57f74bc54488..dd29a443cd3a 100644 --- a/packages/provider-cursor/src/server/usageLimits.ts +++ b/packages/provider-cursor/src/server/usageLimits.ts @@ -1,7 +1,6 @@ -import * as NodeOS from "node:os"; import type { ServerProviderUsageWindow } from "@t3tools/contracts"; import type { CursorSettings } from "../settings.ts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { CURSOR_USAGE_WINDOWS } from "@t3tools/shared/usageLimits"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; @@ -109,7 +108,8 @@ export const readCursorUsageLimits = Effect.fn("readCursorUsageLimits")(function token = (yield* keychain.accessToken)?.trim(); } else if (!token) { const home = - (platform === "win32" ? environment.USERPROFILE : environment.HOME) || NodeOS.homedir(); + (platform === "win32" ? environment.USERPROFILE : environment.HOME) || + (yield* HostProcessHomeDirectory); const directory = platform === "win32" ? path.join(environment.APPDATA || path.join(home, "AppData", "Roaming"), "Cursor") diff --git a/packages/provider-grok/src/server/adapter.ts b/packages/provider-grok/src/server/adapter.ts index 8100246ace18..cca040e81698 100644 --- a/packages/provider-grok/src/server/adapter.ts +++ b/packages/provider-grok/src/server/adapter.ts @@ -4,7 +4,11 @@ import { isXAiTaskCompletedWakeNotification, xAiRateLimitedErrorCode, } from "./xaiAcpExtension.ts"; -import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { + HostProcessEnvironment, + HostProcessHomeDirectory, + HostProcessPlatform, +} from "@t3tools/shared/hostProcess"; import { resolveSelfInvocation, type SelfInvocation } from "@t3tools/shared/nodeRuntime"; import { defaultInstanceIdForDriver, @@ -227,7 +231,10 @@ export function grokLaunchRuntimeMode( } /** The flavor runs Grok's launcher through the adapter's spawner. */ -export function makeGrokAcpAdapterFlavor(options: GrokAdapterV2Options): AcpAdapterV2Flavor { +export function makeGrokAcpAdapterFlavor( + options: GrokAdapterV2Options, + homeDirectory: string, +): AcpAdapterV2Flavor { return { driver: GROK_PROVIDER, runtimeHarness: "Grok", @@ -311,6 +318,7 @@ export function makeGrokAcpAdapterFlavor(options: GrokAdapterV2Options): AcpAdap extractGrokPlanMarkdownFromToolCallData(toolCall.data, { platform: options.hostPlatform, environment: options.environment, + homeDirectory, }), extractBackgroundTaskId: extractXAiMonitorTaskId, extractBackgroundToolMutation: extractXAiAcpBackgroundToolMutation, @@ -331,7 +339,7 @@ export const makeGrokAdapterV2 = Effect.fn("makeGrokAdapterV2")(function* ( ) { return yield* makeAcpAdapterV2({ instanceId: options.instanceId, - flavor: makeGrokAcpAdapterFlavor(options), + flavor: makeGrokAcpAdapterFlavor(options, yield* HostProcessHomeDirectory), selfInvocation: options.selfInvocation, ...(options.nativeLogging === undefined ? {} : { nativeLogging: options.nativeLogging }), ...(options.continuationRequests === undefined @@ -366,7 +374,7 @@ export const GrokAdapterV2Driver: ProviderAdapterDriver const path = yield* Path.Path; const host = yield* ProviderHost.ProviderHost; const { cwd } = host.paths; - const processEnv = mergeProviderInstanceEnvironment(environment); + const processEnv = yield* mergeProviderInstanceEnvironment(environment); const continuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, instanceId, diff --git a/packages/provider-grok/src/server/usage.ts b/packages/provider-grok/src/server/usage.ts index 9f2be3c04299..8b5c0db45ea6 100644 --- a/packages/provider-grok/src/server/usage.ts +++ b/packages/provider-grok/src/server/usage.ts @@ -4,7 +4,6 @@ * * @module provider-grok/server/usage */ -import * as NodeOS from "node:os"; import type { UsageTokenTotals } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; @@ -20,6 +19,7 @@ import { } from "@t3tools/provider-core/server/usage"; import type { GrokSettings } from "../settings.ts"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; /** * Grok reports cost in integer ticks where `1 USD = 10^10` ticks. See Grok @@ -229,8 +229,10 @@ export const grokUsageReader: ProviderUsageReader = { format: grokUsageFormat, directories: Effect.fn("grokUsageReader.directories")(function* ({ environment }) { const path = yield* Path.Path; + const homeDirectory = yield* HostProcessHomeDirectory; const home = expandHomePath( - environment.GROK_HOME?.trim() || path.join(NodeOS.homedir(), ".grok"), + environment.GROK_HOME?.trim() || path.join(homeDirectory, ".grok"), + homeDirectory, ); // Sessions also ship multi-megabyte `chat_history` and `events` logs that // never carry usage; only `updates.jsonl` does. diff --git a/packages/provider-grok/src/server/usageLimits.ts b/packages/provider-grok/src/server/usageLimits.ts index 83369c062c16..b7db7f6fd79a 100644 --- a/packages/provider-grok/src/server/usageLimits.ts +++ b/packages/provider-grok/src/server/usageLimits.ts @@ -1,4 +1,3 @@ -import * as NodeOS from "node:os"; import type { ServerProviderUsageWindow } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; @@ -14,6 +13,7 @@ import { makeUnavailableUsageLimits, makeUsageLimits, } from "@t3tools/provider-core/server/usageLimits"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const GrokCredentials = Schema.Record( Schema.String, @@ -100,7 +100,10 @@ const readGrokCredential = Effect.fn("readGrokCredential")(function* ( const path = yield* Path.Path; const home = environment.GROK_HOME?.trim() || - path.join(environment.HOME || environment.USERPROFILE || NodeOS.homedir(), ".grok"); + path.join( + environment.HOME || environment.USERPROFILE || (yield* HostProcessHomeDirectory), + ".grok", + ); for (const configPath of [ path.join(home, "config.toml"), path.join(home, "managed_config.toml"), diff --git a/packages/provider-grok/src/server/xaiAcpExtension.test.ts b/packages/provider-grok/src/server/xaiAcpExtension.test.ts index 5dce41edead9..18803e0f39e4 100644 --- a/packages/provider-grok/src/server/xaiAcpExtension.test.ts +++ b/packages/provider-grok/src/server/xaiAcpExtension.test.ts @@ -1,5 +1,3 @@ -// @effect-diagnostics nodeBuiltinImport:off -- plan-path tests resolve the OS home dir. -import * as NodeOS from "node:os"; import { it } from "@effect/vitest"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; @@ -1902,13 +1900,14 @@ describe("Grok exit_plan_mode capture (#8358)", () => { expect(extractXAiExitPlanMarkdown(wrapped)).toBe(XAI_EMPTY_PLAN_MARKDOWN); }); it("identifies Grok plan.md paths and extracts markdown from tool call data", () => { - const linuxHost = { platform: "linux" as const, environment: {} }; - const windowsHost = { platform: "win32" as const, environment: {} }; + const home = "/home/ada"; + const linuxHost = { platform: "linux" as const, environment: {}, homeDirectory: home }; + const windowsHost = { platform: "win32" as const, environment: {}, homeDirectory: home }; const grokHomeHost = { platform: "linux" as const, environment: { GROK_HOME: "/opt/grok-data" }, + homeDirectory: home, }; - const home = NodeOS.homedir().replace(/\\/g, "/"); const sessionPlan = `${home}/.grok/sessions/abc/plan.md`; const nestedSessionPlan = `${home}/.grok/sessions/%2Fhome%2Fproj/019fd20e-c563-70a0-b801-a6bc51815a9b/plan.md`; expect(isGrokPlanMarkdownPath(sessionPlan, linuxHost)).toBe(true); @@ -1935,6 +1934,7 @@ describe("Grok exit_plan_mode capture (#8358)", () => { isGrokPlanMarkdownPath("/OPT/GROK-DATA/sessions/sess/plan.md", { platform: "win32", environment: { GROK_HOME: "/opt/grok-data" }, + homeDirectory: home, }), ).toBe(true); expect(isGrokPlanMarkdownPath("/OPT/GROK-DATA/sessions/sess/plan.md", grokHomeHost)).toBe( diff --git a/packages/provider-grok/src/server/xaiAcpExtension.ts b/packages/provider-grok/src/server/xaiAcpExtension.ts index 4c424a664d90..3fe2c2886dea 100644 --- a/packages/provider-grok/src/server/xaiAcpExtension.ts +++ b/packages/provider-grok/src/server/xaiAcpExtension.ts @@ -1,6 +1,3 @@ -// @effect-diagnostics nodeBuiltinImport:off -- Grok's plan file lives under the OS home dir. -import * as NodeOS from "node:os"; - import type { ProviderUserInputAnswers, UserInputQuestion } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Deferred from "effect/Deferred"; @@ -1088,11 +1085,13 @@ function addGrokSessionPrefix( export interface GrokPlanPathHost { readonly platform: NodeJS.Platform; readonly environment: NodeJS.ProcessEnv; + readonly homeDirectory: string; } -function grokPlanSessionPrefixes(environment: NodeJS.ProcessEnv): ReadonlySet { +function grokPlanSessionPrefixes(host: GrokPlanPathHost): ReadonlySet { + const { environment } = host; const prefixes = new Set(); - addGrokSessionPrefix(prefixes, NodeOS.homedir(), true); + addGrokSessionPrefix(prefixes, host.homeDirectory, true); addGrokSessionPrefix(prefixes, "~", true); addGrokSessionPrefix(prefixes, environment.HOME ?? "", true); addGrokSessionPrefix(prefixes, environment.USERPROFILE ?? "", true); @@ -1134,7 +1133,7 @@ export function isGrokPlanMarkdownPath( ) { return false; } - for (const prefix of grokPlanSessionPrefixes(host.environment)) { + for (const prefix of grokPlanSessionPrefixes(host)) { const needle = win32 ? prefix.toLowerCase() : prefix; if (!haystack.startsWith(needle)) { continue; diff --git a/packages/provider-muse/src/server/driver.ts b/packages/provider-muse/src/server/driver.ts index 1d2922e54270..3b9df42100a6 100644 --- a/packages/provider-muse/src/server/driver.ts +++ b/packages/provider-muse/src/server/driver.ts @@ -1,6 +1,6 @@ import { ProviderDriverKind } from "@t3tools/contracts"; import { MuseSettings } from "../settings.ts"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; @@ -71,14 +71,14 @@ export const MuseDriver: ProviderDriver = { const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; const hostEnvironment = yield* HostProcessEnvironment; // Drop an inherited META_API_KEY so Muse uses its login; an instance value still wins. - const processEnvironment = mergeProviderInstanceEnvironment( + const processEnvironment = yield* mergeProviderInstanceEnvironment( environment, makeMuseEnvironment(hostEnvironment), ); const effectiveConfig = { ...config, enabled, - binaryPath: expandHomePath(config.binaryPath), + binaryPath: expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory), } satisfies MuseSettings; const continuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, diff --git a/packages/provider-opencode/src/server/adapter.ts b/packages/provider-opencode/src/server/adapter.ts index 96d535070294..c7020b8ff45a 100644 --- a/packages/provider-opencode/src/server/adapter.ts +++ b/packages/provider-opencode/src/server/adapter.ts @@ -3744,7 +3744,7 @@ export const OpenCodeAdapterV2Driver: ProviderAdapterDriver< return yield* makeOpenCodeAdapterV2({ instanceId: input.instanceId, settings: { ...input.config, enabled: input.enabled }, - environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment), + environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment), ...(providerEventLoggers.native === undefined ? {} : { nativeEventLogger: providerEventLoggers.native }), diff --git a/packages/provider-opencode/src/server/driver.ts b/packages/provider-opencode/src/server/driver.ts index 0573db664969..f3340a62e9e0 100644 --- a/packages/provider-opencode/src/server/driver.ts +++ b/packages/provider-opencode/src/server/driver.ts @@ -206,7 +206,7 @@ export const OpenCodeDriver: ProviderDriver< const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions; const crypto = yield* Crypto.Crypto; const host = yield* ProviderHost.ProviderHost; - const processEnv = mergeProviderInstanceEnvironment(environment); + const processEnv = yield* mergeProviderInstanceEnvironment(environment); const continuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, instanceId, diff --git a/packages/provider-opencode/src/server/usage.ts b/packages/provider-opencode/src/server/usage.ts index f0005ad61608..65897f85d98e 100644 --- a/packages/provider-opencode/src/server/usage.ts +++ b/packages/provider-opencode/src/server/usage.ts @@ -4,7 +4,6 @@ * * @module provider-opencode/server/usage */ -import * as NodeOS from "node:os"; import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; import { @@ -12,7 +11,7 @@ import { type ProviderUsageReader, type UsageRecord, } from "@t3tools/provider-core/server/usage"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; @@ -259,6 +258,7 @@ const resolveOpenCodeDataDirs = Effect.fn("resolveOpenCodeDataDirs")(function* ( const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const environment = yield* HostProcessEnvironment; + const homeDirectory = yield* HostProcessHomeDirectory; const roots = environment["OPENCODE_DATA_DIR"] ?.split(",") .map((value) => value.trim()) @@ -268,13 +268,13 @@ const resolveOpenCodeDataDirs = Effect.fn("resolveOpenCodeDataDirs")(function* ( path.join( dataHome && path.isAbsolute(dataHome) ? dataHome - : path.join(NodeOS.homedir(), ".local", "share"), + : path.join(homeDirectory, ".local", "share"), "opencode", ), ]; const canonical = new Set(); for (const root of roots?.length ? roots : defaults) { - const resolved = path.resolve(expandHomePath(root)); + const resolved = path.resolve(expandHomePath(root, homeDirectory)); canonical.add(yield* fileSystem.realPath(resolved).pipe(Effect.orElseSucceed(() => resolved))); } return [...canonical]; diff --git a/packages/provider-opencode/src/server/usageLimits.ts b/packages/provider-opencode/src/server/usageLimits.ts index 3c21536b8798..05c3b79d411a 100644 --- a/packages/provider-opencode/src/server/usageLimits.ts +++ b/packages/provider-opencode/src/server/usageLimits.ts @@ -1,5 +1,3 @@ -import * as NodeOS from "node:os"; - import type { ServerProviderUsageWindow } from "@t3tools/contracts"; import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; @@ -18,6 +16,7 @@ import { makeUnavailableUsageLimits, makeUsageLimits, } from "@t3tools/provider-core/server/usageLimits"; +import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const AuthFile = Schema.Struct({ "opencode-go": Schema.optionalKey(Schema.Unknown) }); const ApiAuth = Schema.Struct({ type: Schema.Literal("api"), key: Schema.String }); @@ -47,7 +46,11 @@ export const readOpenCodeGoUsageLimits = Effect.fn("readOpenCodeGoUsageLimits")( const env = input.environment; const dataHome = env.XDG_DATA_HOME || - path.join(env.HOME || env.USERPROFILE || NodeOS.homedir(), ".local", "share"); + path.join( + env.HOME || env.USERPROFILE || (yield* HostProcessHomeDirectory), + ".local", + "share", + ); const authPath = path.join(dataHome, "opencode", "auth.json"); const contents = env.OPENCODE_AUTH_CONTENT || diff --git a/packages/provider-pi/src/server/adapter.ts b/packages/provider-pi/src/server/adapter.ts index eecba9c93aad..c6bdc4338ab3 100644 --- a/packages/provider-pi/src/server/adapter.ts +++ b/packages/provider-pi/src/server/adapter.ts @@ -3276,7 +3276,7 @@ export const PiAdapterV2Driver: ProviderAdapterDriver = { const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions; const host = yield* ProviderHost.ProviderHost; const { cwd } = host.paths; - const processEnv = mergeProviderInstanceEnvironment(environment); + const processEnv = yield* mergeProviderInstanceEnvironment(environment); const continuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, instanceId, diff --git a/packages/shared/src/hostProcess.ts b/packages/shared/src/hostProcess.ts index 9bd41a825044..d729624b7630 100644 --- a/packages/shared/src/hostProcess.ts +++ b/packages/shared/src/hostProcess.ts @@ -25,6 +25,13 @@ export const HostProcessHostname = Context.Reference( }, ); +export const HostProcessHomeDirectory = Context.Reference( + "@t3tools/shared/hostProcess/HostProcessHomeDirectory", + { + defaultValue: () => NodeOS.homedir(), + }, +); + export const HostProcessEnvironment = Context.Reference( "@t3tools/shared/hostProcess/HostProcessEnvironment", { diff --git a/packages/source-control-forgejo/src/server/ForgejoCli.ts b/packages/source-control-forgejo/src/server/ForgejoCli.ts index 73b0098aa403..46afc4522b45 100644 --- a/packages/source-control-forgejo/src/server/ForgejoCli.ts +++ b/packages/source-control-forgejo/src/server/ForgejoCli.ts @@ -6,13 +6,12 @@ import * as Result from "effect/Result"; import * as Clock from "effect/Clock"; import * as FileSystem from "effect/FileSystem"; import * as Semaphore from "effect/Semaphore"; -import * as NodeOS from "node:os"; // @effect-diagnostics-next-line nodeBuiltinImport:off - fj storage paths use explicit Windows and POSIX layouts, independently of this process's platform. import * as NodePath from "node:path"; import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http"; import { ChildProcessSpawner } from "effect/process"; import { decodeJsonResult } from "@t3tools/shared/schemaJson"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; import { collectUint8StreamText } from "@t3tools/provider-core/server/collectStreamText"; import type { SourceControlProviderContext } from "@t3tools/source-control-core/server/SourceControlProvider"; @@ -250,7 +249,7 @@ export const make = Effect.gen(function* () { const readKeys = Effect.fn("ForgejoCli.readKeys")(function* (cwd: string) { for (const path of forgejoKeysPaths({ platform: yield* HostProcessPlatform, - home: NodeOS.homedir(), + home: yield* HostProcessHomeDirectory, ...(globalThis.process.env.XDG_DATA_HOME ? { dataHome: globalThis.process.env.XDG_DATA_HOME } : {}), From e30852213ed3954037091582057f71918918e19f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 16:38:41 -0700 Subject: [PATCH 28/39] refactor(shared): host process references live in a HostProcess module (#17641) Co-authored-by: Claude Opus 5.5 --- apps/desktop/src/app/DesktopApp.ts | 4 +- .../src/app/DesktopAppActivation.test.ts | 18 ++--- apps/desktop/src/app/DesktopAppActivation.ts | 4 +- apps/desktop/src/app/DesktopClerk.test.ts | 4 +- apps/desktop/src/app/DesktopClerk.ts | 4 +- .../src/app/DesktopPreReadyFileSystem.test.ts | 4 +- .../src/app/DesktopPreReadyPlatform.test.ts | 12 +-- .../src/app/DesktopPreReadyPlatform.ts | 6 +- .../backend/DesktopNetworkInterfaces.test.ts | 4 +- .../src/backend/DesktopNetworkInterfaces.ts | 4 +- .../desktop/src/electron/ElectronMenu.test.ts | 4 +- apps/desktop/src/electron/ElectronMenu.ts | 4 +- .../src/electron/ElectronSafeStorage.ts | 4 +- .../src/electron/ElectronWindow.test.ts | 4 +- apps/desktop/src/electron/ElectronWindow.ts | 4 +- .../src/ipc/methods/notificationBadge.test.ts | 4 +- .../src/ipc/methods/notificationBadge.ts | 4 +- apps/desktop/src/ipc/methods/window.test.ts | 8 +- apps/desktop/src/ipc/methods/window.ts | 7 +- apps/desktop/src/main.ts | 6 +- .../BrowserImport/BrowserImport.test.ts | 16 ++-- .../preview/BrowserImport/BrowserImport.ts | 6 +- .../preview/BrowserImport/ChromiumCookies.ts | 2 +- .../BrowserImport/ChromiumKeys.test.ts | 6 +- .../src/preview/BrowserImport/ChromiumKeys.ts | 6 +- .../BrowserImport/SafariPermission.test.ts | 10 +-- .../src/preview/BrowserImport/Sources.test.ts | 64 ++++++++-------- .../src/preview/BrowserImport/Sources.ts | 17 ++--- apps/desktop/src/preview/Manager.test.ts | 4 +- apps/desktop/src/preview/Manager.ts | 4 +- apps/desktop/src/snapShot/DesktopSnapShot.ts | 4 +- apps/desktop/src/snapShot/snapShot.test.ts | 4 +- .../record-codex-app-server-replay-fixture.ts | 5 +- .../scripts/record-grok-acp-replay-fixture.ts | 6 +- apps/server/scripts/t3-sqlite-state.test.ts | 4 +- apps/server/src/assets/AssetAccess.test.ts | 4 +- apps/server/src/assets/AssetAccess.ts | 4 +- .../src/assets/NativeAppIconResolver.test.ts | 4 +- .../src/assets/NativeAppIconResolver.ts | 4 +- apps/server/src/bootstrap.test.ts | 8 +- apps/server/src/bootstrap.ts | 4 +- .../src/checkpointing/CheckpointStore.test.ts | 4 +- apps/server/src/cli/app.test.ts | 35 ++++----- apps/server/src/cli/app.ts | 12 +-- apps/server/src/cli/browser.ts | 12 +-- apps/server/src/cli/connect.ts | 4 +- apps/server/src/cli/invocation.test.ts | 42 +++++------ apps/server/src/cli/invocation.ts | 26 +++---- apps/server/src/cli/pair.test.ts | 4 +- apps/server/src/cli/server.ts | 4 +- apps/server/src/cli/service.test.ts | 8 +- apps/server/src/cli/service.ts | 4 +- apps/server/src/cli/theme.test.ts | 4 +- apps/server/src/cli/triage.ts | 4 +- apps/server/src/cli/uninstall.test.ts | 4 +- apps/server/src/cli/uninstall.ts | 16 ++-- apps/server/src/cli/update.test.ts | 31 ++++---- apps/server/src/cli/update.ts | 31 +++----- apps/server/src/cloud/bootService.test.ts | 12 +-- apps/server/src/cloud/bootService.ts | 15 ++-- apps/server/src/cloud/selfUpdate.test.ts | 6 +- apps/server/src/cloud/selfUpdate.ts | 6 +- .../src/cloud/serviceLauncherClient.test.ts | 4 +- .../server/src/cloud/serviceLauncherClient.ts | 4 +- apps/server/src/device/AgentDeviceShim.ts | 4 +- .../src/device/AgentDeviceTarget.test.ts | 12 +-- .../server/src/device/LocalDeviceHost.test.ts | 25 +++---- apps/server/src/device/LocalDeviceHost.ts | 24 +++--- .../server/src/device/sshDeviceScript.test.ts | 8 +- .../src/environment/RemoteOpenTargets.test.ts | 4 +- .../src/environment/RemoteOpenTargets.ts | 4 +- .../src/environment/ServerEnvironment.test.ts | 15 ++-- .../src/environment/ServerEnvironment.ts | 6 +- .../ServerEnvironmentLabel.test.ts | 6 +- .../src/environment/ServerEnvironmentLabel.ts | 6 +- .../ServerEnvironmentMachine.test.ts | 4 +- .../environment/ServerEnvironmentMachine.ts | 4 +- apps/server/src/htmlRender/HtmlRender.test.ts | 10 +-- apps/server/src/htmlRender/HtmlRender.ts | 4 +- apps/server/src/keybindings.test.ts | 4 +- .../src/mcp/toolkits/device/handlers.ts | 4 +- apps/server/src/observability/HeapSnapshot.ts | 4 +- .../Adapters/AcpAdapterV2.test.ts | 18 ++--- .../Adapters/ClaudeAdapterV2.test.ts | 4 +- .../Adapters/ClaudeAdapterV2.testkit.ts | 4 +- .../Adapters/ClaudeAdapterV2.ts | 8 +- .../Adapters/CodexAdapterV2.test.ts | 12 +-- .../Adapters/CodexAdapterV2.ts | 8 +- .../Adapters/CursorAdapterV2.testkit.ts | 4 +- .../Adapters/GrokAdapterV2.test.ts | 4 +- .../Adapters/GrokAdapterV2.testkit.ts | 4 +- .../orchestration-v2/workflowScriptQuery.ts | 4 +- apps/server/src/os-jank.ts | 14 ++-- apps/server/src/preview/PortScanner.test.ts | 8 +- apps/server/src/preview/PortScanner.ts | 4 +- .../server/src/preview/PreviewBrowser.test.ts | 8 +- apps/server/src/preview/PreviewBrowser.ts | 6 +- .../src/preview/PreviewBrowserHost.test.ts | 4 +- apps/server/src/preview/PreviewBrowserHost.ts | 6 +- apps/server/src/preview/ServerBrowser.ts | 10 +-- .../src/process/externalLauncher.test.ts | 10 +-- apps/server/src/process/externalLauncher.ts | 10 +-- apps/server/src/processRunner.test.ts | 8 +- .../server/src/project/AgentSessionScanner.ts | 12 +-- .../src/project/ProjectSetupScriptRunner.ts | 6 +- .../provider/AntigravityInstallation.test.ts | 19 ++--- .../src/provider/AntigravityInstallation.ts | 14 ++-- .../src/provider/CodexInstallation.test.ts | 12 +-- apps/server/src/provider/CodexInstallation.ts | 16 ++-- apps/server/src/provider/CodexProvider.ts | 4 +- .../Drivers/AntigravityDriver.test.ts | 17 ++--- .../src/provider/Drivers/AntigravityDriver.ts | 6 +- .../src/provider/Drivers/AntigravityUsage.ts | 6 +- .../src/provider/Drivers/ClaudeDriver.ts | 4 +- .../provider/Drivers/ClaudeExecutable.test.ts | 6 +- .../src/provider/Drivers/ClaudeExecutable.ts | 4 +- .../server/src/provider/Drivers/ClaudeHome.ts | 4 +- .../src/provider/Drivers/ClaudeSkills.ts | 6 +- .../src/provider/Drivers/CodexDriver.test.ts | 4 +- .../src/provider/Drivers/CodexDriver.ts | 4 +- .../src/provider/Drivers/CodexHomeLayout.ts | 4 +- .../src/provider/Drivers/claudeUsage.ts | 4 +- .../provider/ProviderInstanceRegistry.test.ts | 14 ++-- .../acp/AcpSessionRuntime.processTree.test.ts | 10 +-- .../provider/antigravityAuthSupport.test.ts | 14 ++-- .../src/provider/antigravityAuthSupport.ts | 6 +- .../src/provider/claudeResetCredits.test.ts | 12 +-- .../server/src/provider/claudeResetCredits.ts | 6 +- .../src/provider/providerInstallation.test.ts | 4 +- .../providerMaintenanceRunner.test.ts | 8 +- .../src/resourceTelemetry/HostResources.ts | 4 +- .../ResourceMonitorBinary.test.ts | 44 +++++------ .../ResourceMonitorBinary.ts | 12 +-- apps/server/src/review/ReviewService.ts | 4 +- .../SourceControlRepositoryService.ts | 4 +- apps/server/src/storageCleanup.ts | 4 +- .../src/telemetry/AnalyticsService.test.ts | 14 ++-- apps/server/src/telemetry/AnalyticsService.ts | 6 +- apps/server/src/telemetry/Identify.ts | 4 +- apps/server/src/terminal/Manager.test.ts | 12 +-- apps/server/src/terminal/Manager.ts | 12 +-- .../src/terminal/NodePtyAdapter.test.ts | 10 +-- apps/server/src/terminal/NodePtyAdapter.ts | 16 ++-- .../ClaudeTextGeneration.test.ts | 6 +- .../src/textGeneration/CodexTextGeneration.ts | 4 +- apps/server/src/usage/UsageService.test.ts | 10 +-- apps/server/src/usage/UsageService.ts | 4 +- apps/server/src/vcs/GitVcsDriverCore.test.ts | 10 +-- apps/server/src/vcs/GitVcsDriverCore.ts | 8 +- apps/server/src/vcs/VcsProcess.test.ts | 4 +- .../src/workspace/WorkspaceEntries.test.ts | 6 +- apps/server/src/workspace/WorkspaceEntries.ts | 8 +- .../src/workspace/WorkspaceFileSystem.test.ts | 4 +- apps/server/src/workspace/WorkspacePaths.ts | 4 +- apps/server/src/ws.test.ts | 4 +- .../rules/no-global-process-runtime.test.ts | 12 +-- .../rules/no-global-process-runtime.ts | 2 +- .../src/server/AcpRegistrySupport.test.ts | 32 ++++---- .../src/server/AcpRegistrySupport.ts | 12 +-- .../src/server/adapter.ts | 4 +- .../src/server/driver.ts | 4 +- .../src/server/AcpSessionRuntime.ts | 6 +- .../src/server/instanceEnvironment.test.ts | 4 +- .../src/server/instanceEnvironment.ts | 4 +- .../src/server/maintenanceResolver.test.ts | 20 ++--- .../src/server/maintenanceResolver.ts | 8 +- .../provider-core/src/server/pathExpansion.ts | 2 +- .../src/server/processGroup.test.ts | 4 +- .../src/server/snapshotProbe.test.ts | 4 +- .../provider-core/src/server/snapshotProbe.ts | 4 +- .../src/server/CursorUsageAccounts.ts | 12 +-- .../provider-cursor/src/server/adapter.ts | 4 +- packages/provider-cursor/src/server/skills.ts | 4 +- .../src/server/textGeneration.ts | 4 +- .../src/server/usageLimits.test.ts | 12 +-- .../provider-cursor/src/server/usageLimits.ts | 6 +- .../provider-grok/src/server/acpSupport.ts | 4 +- packages/provider-grok/src/server/adapter.ts | 16 ++-- .../provider-grok/src/server/driver.test.ts | 4 +- packages/provider-grok/src/server/usage.ts | 4 +- .../provider-grok/src/server/usageLimits.ts | 4 +- packages/provider-muse/src/server/driver.ts | 6 +- .../OpenCodeRuntime.environment.test.ts | 12 +-- .../server/OpenCodeRuntime.inventory.test.ts | 14 ++-- .../src/server/OpenCodeRuntime.ts | 4 +- .../src/server/OpenCodeServerLedger.test.ts | 6 +- .../src/server/OpenCodeServerLedger.ts | 4 +- .../provider-opencode/src/server/adapter.ts | 6 +- .../src/server/driver.test.ts | 6 +- .../provider-opencode/src/server/usage.ts | 6 +- .../src/server/usageLimits.ts | 4 +- .../src/server/v2/OpenCode2Server.test.ts | 14 ++-- .../provider-pi/src/server/adapter.test.ts | 4 +- packages/provider-pi/src/server/adapter.ts | 6 +- packages/provider-pi/src/server/rpc.ts | 4 +- packages/provider-testing/src/fakeCli.ts | 6 +- packages/shared/package.json | 6 +- .../src/{hostProcess.ts => HostProcess.ts} | 75 ++++++++----------- packages/shared/src/editor.ts | 4 +- packages/shared/src/logging.test.ts | 4 +- packages/shared/src/nodeRuntime.test.ts | 61 +++++++-------- packages/shared/src/nodeRuntime.ts | 22 ++---- packages/shared/src/relayClient.test.ts | 8 +- packages/shared/src/relayClient.ts | 6 +- packages/shared/src/shell.test.ts | 26 +++---- packages/shared/src/shell.ts | 12 +-- packages/shared/src/testing/longTempDir.ts | 4 +- .../src/server/ForgejoCli.ts | 6 +- .../src/server/GitHubCredentials.ts | 6 +- .../GitHubSourceControlProvider.test.ts | 6 +- .../src/server/GitHubSourceControlProvider.ts | 6 +- packages/ssh/src/auth.test.ts | 6 +- packages/ssh/src/auth.ts | 8 +- packages/ssh/src/command.ts | 4 +- packages/ssh/src/runnerProcess.test.ts | 6 +- packages/ssh/src/tunnel.test.ts | 6 +- packages/tailscale/src/tailscale.test.ts | 4 +- packages/tailscale/src/tailscale.ts | 6 +- scripts/build-cli-archive.ts | 6 +- scripts/build-desktop-artifact.test.ts | 34 ++++----- scripts/build-desktop-artifact.ts | 14 ++-- scripts/build-npm-platform-packages.test.ts | 6 +- scripts/build-npm-platform-packages.ts | 6 +- scripts/dev-runner.test.ts | 36 ++++----- scripts/dev-runner.ts | 8 +- scripts/export-brand-icons.ts | 4 +- scripts/install.test.ts | 6 +- scripts/lib/build-target-arch.test.ts | 6 +- scripts/lib/build-target-arch.ts | 6 +- scripts/lib/cli-external-packages.test.ts | 2 +- scripts/mobile-native-client.ts | 18 ++--- scripts/mobile-native-static-check.test.ts | 4 +- scripts/smoke-cli-archive.ts | 4 +- vite.config.ts | 2 +- 234 files changed, 948 insertions(+), 1145 deletions(-) rename packages/shared/src/{hostProcess.ts => HostProcess.ts} (53%) diff --git a/apps/desktop/src/app/DesktopApp.ts b/apps/desktop/src/app/DesktopApp.ts index b76f6b4ba197..a6906099dbd5 100644 --- a/apps/desktop/src/app/DesktopApp.ts +++ b/apps/desktop/src/app/DesktopApp.ts @@ -35,7 +35,7 @@ import * as DesktopUpdates from "../updates/DesktopUpdates.ts"; import * as DesktopSnapShot from "../snapShot/DesktopSnapShot.ts"; import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts"; import * as DesktopRendererHistory from "../telemetry/DesktopRendererHistory.ts"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; const DEFAULT_DESKTOP_BACKEND_PORT = 3773; const MAX_TCP_PORT = 65_535; @@ -286,7 +286,7 @@ const startup = Effect.gen(function* () { const linuxElectronOptions = environment.platform === "linux" && !hasCommandLinePasswordStore ? DesktopPreReadyPlatform.resolveEarlyLinuxElectronOptionsFromProcess( - yield* HostProcessHomeDirectory, + yield* HostProcess.HomeDirectory, ) : preReadyElectronOptions.linux; if (linuxElectronOptions !== null && !hasCommandLinePasswordStore) { diff --git a/apps/desktop/src/app/DesktopAppActivation.test.ts b/apps/desktop/src/app/DesktopAppActivation.test.ts index 8ab165afce0a..dfe415644760 100644 --- a/apps/desktop/src/app/DesktopAppActivation.test.ts +++ b/apps/desktop/src/app/DesktopAppActivation.test.ts @@ -11,7 +11,7 @@ import { type DesktopAppActivationResponse, } from "@t3tools/contracts"; import { resolveDesktopAppControlAddress } from "@t3tools/shared/desktopAppControl"; -import { HostProcessPlatform, HostProcessUserId } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import { afterEach, describe, expect } from "vite-plus/test"; @@ -83,8 +83,8 @@ function exchange(address: string, payload: DesktopAppActivationRequest) { describe("desktop app control server", () => { it.effect("roundtrips a request and removes its socket on shutdown", () => Effect.gen(function* () { - const platform = yield* HostProcessPlatform; - const userId = yield* HostProcessUserId; + const platform = yield* HostProcess.Platform; + const userId = yield* HostProcess.UserId; yield* Effect.promise(async () => { const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-app-control-test-")); const target = makeTarget(NodePath.join(root, "userdata"), platform, userId); @@ -123,8 +123,8 @@ describe("desktop app control server", () => { it.effect("cancels a queued request when the client disconnects", () => Effect.gen(function* () { - const platform = yield* HostProcessPlatform; - const userId = yield* HostProcessUserId; + const platform = yield* HostProcess.Platform; + const userId = yield* HostProcess.UserId; yield* Effect.promise(async () => { const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-app-cancel-test-")); const target = makeTarget(NodePath.join(root, "userdata"), platform, userId); @@ -162,8 +162,8 @@ describe("desktop app control server", () => { // Two desktop apps can share one state dir, such as nightly and a preview build. it.effect("keeps a newer app's socket when an older app on the same state dir quits", () => Effect.gen(function* () { - const platform = yield* HostProcessPlatform; - const userId = yield* HostProcessUserId; + const platform = yield* HostProcess.Platform; + const userId = yield* HostProcess.UserId; if (platform === "win32") return; yield* Effect.promise(async () => { const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-app-takeover-test-")); @@ -183,8 +183,8 @@ describe("desktop app control server", () => { it.effect("binds its address again after the socket file is removed", () => Effect.gen(function* () { - const platform = yield* HostProcessPlatform; - const userId = yield* HostProcessUserId; + const platform = yield* HostProcess.Platform; + const userId = yield* HostProcess.UserId; if (platform === "win32") return; yield* Effect.promise(async () => { const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-app-reclaim-test-")); diff --git a/apps/desktop/src/app/DesktopAppActivation.ts b/apps/desktop/src/app/DesktopAppActivation.ts index 740799955da2..19f24460106e 100644 --- a/apps/desktop/src/app/DesktopAppActivation.ts +++ b/apps/desktop/src/app/DesktopAppActivation.ts @@ -12,7 +12,7 @@ import { type DesktopAppActivationResponse, } from "@t3tools/contracts"; import { resolveDesktopAppControlAddress } from "@t3tools/shared/desktopAppControl"; -import { HostProcessUserId } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -315,7 +315,7 @@ export const make = Effect.gen(function* () { const desktopWindow = yield* DesktopWindow.DesktopWindow; const electronWindow = yield* ElectronWindow.ElectronWindow; const path = yield* Path.Path; - const userId = yield* HostProcessUserId; + const userId = yield* HostProcess.UserId; const runPromise = Effect.runPromiseWith(yield* Effect.context()); const address = resolveDesktopAppControlAddress({ stateDir: path.resolve(desktopEnvironment.stateDir), diff --git a/apps/desktop/src/app/DesktopClerk.test.ts b/apps/desktop/src/app/DesktopClerk.test.ts index 4b4b90c29728..76306bfbf227 100644 --- a/apps/desktop/src/app/DesktopClerk.test.ts +++ b/apps/desktop/src/app/DesktopClerk.test.ts @@ -3,7 +3,7 @@ import * as NodeHttp from "node:http"; import * as NodePath from "@effect/platform-node/NodePath"; import { codexAuthHandoffUrl, readCodexAuthDelivery } from "@t3tools/shared/codexAuthHandoff"; import { EnvironmentId, ProviderInstanceId } from "@t3tools/contracts"; -import { HostProcessArguments } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { assert, describe, it } from "@effect/vitest"; import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; @@ -375,7 +375,7 @@ it.effect.each(["startup", "open-url"] as const)( assert.strictEqual(delivery?.returnUrl, request.returnUrl); }).pipe( Effect.provide(layerDesktopClerk(true, [], "darwin", undefined, shell)), - Effect.provideService(HostProcessArguments, entry === "startup" ? ["t3", link] : ["t3"]), + Effect.provideService(HostProcess.Arguments, entry === "startup" ? ["t3", link] : ["t3"]), Effect.provideService(ElectronApp.ElectronApp, electronApp), Effect.provideService( ElectronWindow.ElectronWindow, diff --git a/apps/desktop/src/app/DesktopClerk.ts b/apps/desktop/src/app/DesktopClerk.ts index 1bc390ac3c62..f1a7f31174ab 100644 --- a/apps/desktop/src/app/DesktopClerk.ts +++ b/apps/desktop/src/app/DesktopClerk.ts @@ -11,7 +11,7 @@ import { codexAuthDeliveryUrl, readCodexAuthHandoff } from "@t3tools/shared/code import { receiveCodexAuthCallback, CodexAuthCallbackError } from "./CodexAuthCallback.ts"; import * as ElectronShell from "../electron/ElectronShell.ts"; import { providerAuthReturnUrl } from "@t3tools/shared/providerAuthReturnUrl"; -import { HostProcessArguments } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { clerkFrontendApiHostnameFromPublishableKey } from "@t3tools/shared/relayAuth"; import * as ElectronApp from "../electron/ElectronApp.ts"; import * as ElectronProtocol from "../electron/ElectronProtocol.ts"; @@ -182,7 +182,7 @@ export const make = Effect.gen(function* () { ); return true; }; - const args = yield* HostProcessArguments; + const args = yield* HostProcess.Arguments; args.some((value) => startProviderAuthHandoff(value)); yield* electronApp.on("open-url", (event: { preventDefault: () => void }, url: string) => { if (startProviderAuthHandoff(url) || resumeProviderAuth(url)) event.preventDefault(); diff --git a/apps/desktop/src/app/DesktopPreReadyFileSystem.test.ts b/apps/desktop/src/app/DesktopPreReadyFileSystem.test.ts index ab4c4f86cdf1..542f6b987594 100644 --- a/apps/desktop/src/app/DesktopPreReadyFileSystem.test.ts +++ b/apps/desktop/src/app/DesktopPreReadyFileSystem.test.ts @@ -1,6 +1,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as FileSystem from "effect/FileSystem"; @@ -32,7 +32,7 @@ it.layer(NodeServices.layer)("DesktopPreReadyFileSystem", (it) => { }), ); - it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32" || process.getuid?.() === 0)( + it.effect.skipIf(HostProcess.Platform.defaultValue() === "win32" || process.getuid?.() === 0)( "fails instead of treating an unreadable profile as missing", () => Effect.gen(function* () { diff --git a/apps/desktop/src/app/DesktopPreReadyPlatform.test.ts b/apps/desktop/src/app/DesktopPreReadyPlatform.test.ts index 4fa5d417aa64..ecdc94d8254e 100644 --- a/apps/desktop/src/app/DesktopPreReadyPlatform.test.ts +++ b/apps/desktop/src/app/DesktopPreReadyPlatform.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "@effect/vitest"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -75,7 +75,7 @@ describe("DesktopPreReadyPlatform", () => { }).pipe( Effect.provide( DesktopPreReadyPlatform.layer.pipe( - Layer.provide(Layer.succeed(HostProcessPlatform, "linux")), + Layer.provide(Layer.succeed(HostProcess.Platform, "linux")), ), ), ); @@ -111,7 +111,7 @@ describe("DesktopPreReadyPlatform", () => { })); yield* Layer.build( DesktopPreReadyPlatform.layer.pipe( - Layer.provide(Layer.succeed(HostProcessPlatform, "linux")), + Layer.provide(Layer.succeed(HostProcess.Platform, "linux")), ), ); const identity = yield* Effect.promise(() => portalIdentity); @@ -135,7 +135,7 @@ describe("DesktopPreReadyPlatform", () => { }); return DesktopPreReadyPlatform.make.pipe( - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), Effect.asVoid, ); }); @@ -151,7 +151,7 @@ describe("DesktopPreReadyPlatform", () => { assert.include(contents, "MimeType=x-scheme-handler/t3code;"); assert.include(contents, "Icon="); assert.equal(setDesktopNameMock.mock.calls.length, 1); - }).pipe(Effect.provideService(HostProcessPlatform, "linux")); + }).pipe(Effect.provideService(HostProcess.Platform, "linux")); }); it.effect( @@ -168,7 +168,7 @@ describe("DesktopPreReadyPlatform", () => { }); const layerPreReady = DesktopPreReadyPlatform.layer.pipe( - Layer.provide(Layer.succeed(HostProcessPlatform, "darwin")), + Layer.provide(Layer.succeed(HostProcess.Platform, "darwin")), ); const layerClerkShaped = Layer.effect( diff --git a/apps/desktop/src/app/DesktopPreReadyPlatform.ts b/apps/desktop/src/app/DesktopPreReadyPlatform.ts index 2de132887aa2..378eb58f160c 100644 --- a/apps/desktop/src/app/DesktopPreReadyPlatform.ts +++ b/apps/desktop/src/app/DesktopPreReadyPlatform.ts @@ -6,7 +6,7 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Electron from "electron"; -import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as DesktopEarlyElectronStartup from "./DesktopEarlyElectronStartup.ts"; import { resolveDesktopAppBranding } from "./DesktopEnvironment.ts"; @@ -50,8 +50,8 @@ export class DesktopPreReadyElectronOptions extends Context.Service< /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { - const platform = yield* HostProcessPlatform; - const homeDirectory = yield* HostProcessHomeDirectory; + const platform = yield* HostProcess.Platform; + const homeDirectory = yield* HostProcess.HomeDirectory; return yield* Effect.sync((): DesktopPreReadyElectronOptions["Service"] => { const linuxPasswordStoreCommandLine = platform === "linux" diff --git a/apps/desktop/src/backend/DesktopNetworkInterfaces.test.ts b/apps/desktop/src/backend/DesktopNetworkInterfaces.test.ts index ab6990ef1767..679d19a53d0a 100644 --- a/apps/desktop/src/backend/DesktopNetworkInterfaces.test.ts +++ b/apps/desktop/src/backend/DesktopNetworkInterfaces.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "@effect/vitest"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -16,7 +16,7 @@ vi.mock("node:os", () => ({ import * as DesktopNetworkInterfaces from "./DesktopNetworkInterfaces.ts"; const layerTest = DesktopNetworkInterfaces.layer.pipe( - Layer.provide(Layer.succeed(HostProcessPlatform, "linux")), + Layer.provide(Layer.succeed(HostProcess.Platform, "linux")), ); describe("DesktopNetworkInterfaces", () => { diff --git a/apps/desktop/src/backend/DesktopNetworkInterfaces.ts b/apps/desktop/src/backend/DesktopNetworkInterfaces.ts index de200f1b67b6..f2c32e1d0731 100644 --- a/apps/desktop/src/backend/DesktopNetworkInterfaces.ts +++ b/apps/desktop/src/backend/DesktopNetworkInterfaces.ts @@ -1,6 +1,6 @@ import * as NodeOS from "node:os"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -41,7 +41,7 @@ export class DesktopNetworkInterfaces extends Context.Service< /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; return DesktopNetworkInterfaces.of({ read: Effect.try({ try: () => NodeOS.networkInterfaces(), diff --git a/apps/desktop/src/electron/ElectronMenu.test.ts b/apps/desktop/src/electron/ElectronMenu.test.ts index 01b4fe080304..320988051e5f 100644 --- a/apps/desktop/src/electron/ElectronMenu.test.ts +++ b/apps/desktop/src/electron/ElectronMenu.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "@effect/vitest"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -28,7 +28,7 @@ vi.mock("electron", () => ({ import * as ElectronMenu from "./ElectronMenu.ts"; const layerTest = ElectronMenu.layer.pipe( - Layer.provide(Layer.succeed(HostProcessPlatform, "linux")), + Layer.provide(Layer.succeed(HostProcess.Platform, "linux")), ); const makeWindow = (zoomFactor = 1): Electron.BrowserWindow => diff --git a/apps/desktop/src/electron/ElectronMenu.ts b/apps/desktop/src/electron/ElectronMenu.ts index 401e0c27ccd8..352a5bc97a21 100644 --- a/apps/desktop/src/electron/ElectronMenu.ts +++ b/apps/desktop/src/electron/ElectronMenu.ts @@ -1,5 +1,5 @@ import type { ContextMenuItem } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -114,7 +114,7 @@ const normalizePosition = ( /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; let destructiveMenuIconCache: Option.Option | undefined; const getDestructiveMenuIcon = (): Option.Option => { diff --git a/apps/desktop/src/electron/ElectronSafeStorage.ts b/apps/desktop/src/electron/ElectronSafeStorage.ts index 57ae5d6dcb69..cd01613d92fd 100644 --- a/apps/desktop/src/electron/ElectronSafeStorage.ts +++ b/apps/desktop/src/electron/ElectronSafeStorage.ts @@ -5,7 +5,7 @@ import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import * as Electron from "electron"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; const electronSafeStorageErrorFields = { cause: Schema.Defect(), @@ -67,7 +67,7 @@ export class ElectronSafeStorage extends Context.Service< /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; return ElectronSafeStorage.of({ isEncryptionAvailable: Effect.try({ diff --git a/apps/desktop/src/electron/ElectronWindow.test.ts b/apps/desktop/src/electron/ElectronWindow.test.ts index 50dd15ba6df1..ef94312ed6f0 100644 --- a/apps/desktop/src/electron/ElectronWindow.test.ts +++ b/apps/desktop/src/electron/ElectronWindow.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "@effect/vitest"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; @@ -58,7 +58,7 @@ vi.mock("electron", () => ({ import * as ElectronWindow from "./ElectronWindow.ts"; const layerTestFor = (platform: NodeJS.Platform) => - ElectronWindow.layer.pipe(Layer.provide(Layer.succeed(HostProcessPlatform, platform))); + ElectronWindow.layer.pipe(Layer.provide(Layer.succeed(HostProcess.Platform, platform))); const layerTest = layerTestFor("linux"); diff --git a/apps/desktop/src/electron/ElectronWindow.ts b/apps/desktop/src/electron/ElectronWindow.ts index 1c8d82eb2cf9..a5e74494c12f 100644 --- a/apps/desktop/src/electron/ElectronWindow.ts +++ b/apps/desktop/src/electron/ElectronWindow.ts @@ -2,7 +2,7 @@ import * as NodePath from "node:path"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import type * as Cause from "effect/Cause"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; @@ -129,7 +129,7 @@ export class ElectronWindow extends Context.Service< /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; // The focus worker loads a native accessibility module. Start it on the first // capture reveal so users who never capture pay nothing at launch. let windowsForegroundFocus: ReturnType | undefined; diff --git a/apps/desktop/src/ipc/methods/notificationBadge.test.ts b/apps/desktop/src/ipc/methods/notificationBadge.test.ts index a47732550f64..40d1df6a3366 100644 --- a/apps/desktop/src/ipc/methods/notificationBadge.test.ts +++ b/apps/desktop/src/ipc/methods/notificationBadge.test.ts @@ -1,7 +1,7 @@ import * as Effect from "effect/Effect"; import { beforeEach, expect, vi } from "vite-plus/test"; import { it } from "@effect/vitest"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; const native = vi.hoisted(() => ({ setBadgeCount: vi.fn(), @@ -122,7 +122,7 @@ it.effect("validates IPC and clears on native focus, quit, and disposal", () => expect(native.setBadgeCount).toHaveBeenLastCalledWith(0); }), ).pipe( - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), Effect.provide([ ElectronApp.layer, DesktopIpc.layer({ diff --git a/apps/desktop/src/ipc/methods/notificationBadge.ts b/apps/desktop/src/ipc/methods/notificationBadge.ts index 40e4549138f2..952745b4b7c6 100644 --- a/apps/desktop/src/ipc/methods/notificationBadge.ts +++ b/apps/desktop/src/ipc/methods/notificationBadge.ts @@ -1,7 +1,7 @@ import * as Electron from "electron"; import * as Effect from "effect/Effect"; import * as Schema from "effect/Schema"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as ElectronApp from "../../electron/ElectronApp.ts"; import * as DesktopIpc from "../DesktopIpc.ts"; @@ -45,7 +45,7 @@ export const installNotificationBadge = Effect.fn("desktop.ipc.installNotificati function* () { const ipc = yield* DesktopIpc.DesktopIpc; const app = yield* ElectronApp.ElectronApp; - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const clear = () => { applyNotificationBadge(platform, { count: 0, image: null }); for (const window of Electron.BrowserWindow.getAllWindows()) { diff --git a/apps/desktop/src/ipc/methods/window.test.ts b/apps/desktop/src/ipc/methods/window.test.ts index 72f631d162d1..a6913528653c 100644 --- a/apps/desktop/src/ipc/methods/window.test.ts +++ b/apps/desktop/src/ipc/methods/window.test.ts @@ -1,6 +1,6 @@ import { assert, describe, it } from "@effect/vitest"; import * as NodeServices from "@effect/platform-node/NodeServices"; -import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Effect from "effect/Effect"; @@ -314,7 +314,7 @@ describe("pickProjectFavicon", () => { ); }); -it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")( +it.effect.skipIf(HostProcess.Platform.defaultValue() === "win32")( "finds remote editors installed without PATH launchers", () => Effect.gen(function* () { @@ -333,11 +333,11 @@ it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")( yield* fs.chmod(executable, 0o755); } const editors = yield* probeRemoteEditors.handler(undefined).pipe( - Effect.provideService(HostProcessEnvironment, { + Effect.provideService(HostProcess.Environment, { HOME: home, PATH: path.join(home, "empty"), }), - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Platform, "darwin"), ); assert.include(editors, "cursor"); assert.include(editors, "vscode"); diff --git a/apps/desktop/src/ipc/methods/window.ts b/apps/desktop/src/ipc/methods/window.ts index 8e4ffebfefe7..47e6c02ffe52 100644 --- a/apps/desktop/src/ipc/methods/window.ts +++ b/apps/desktop/src/ipc/methods/window.ts @@ -15,7 +15,7 @@ import { } from "@t3tools/contracts"; import { WORKSPACE_IMAGE_PREVIEW_EXTENSIONS } from "@t3tools/shared/filePreview"; import { resolveEditorCommand } from "@t3tools/shared/editor"; -import * as HostProcess from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Effect from "effect/Effect"; @@ -45,7 +45,6 @@ import { resolveWslPickFolderDefaultPath, wslUncPathToLinuxPath, } from "../../wsl/wslPathParsing.ts"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; const ContextMenuPosition = Schema.Struct({ x: Schema.Number, @@ -345,7 +344,7 @@ export const probeRemoteEditors = DesktopIpc.makeIpcMethod({ result: Schema.Array(EditorId), handler: Effect.fn("desktop.ipc.window.probeRemoteEditors")(function* () { const available: Array = []; - const env = yield* HostProcess.HostProcessEnvironment; + const env = yield* HostProcess.Environment; for (const editorId of REMOTE_CAPABLE_EDITOR_IDS) { const editor = EDITORS.find((editor) => editor.id === editorId); if (editor && Option.isSome(yield* resolveEditorCommand(editor, env))) { @@ -398,7 +397,7 @@ export const pickThemeFiles = DesktopIpc.makeIpcMethod({ // The VS Code extensions directory is the same dotfolder on Windows, // macOS, and Linux; when it is missing the picker opens wherever the // platform would by default. - const extensionsDir = path.join(yield* HostProcessHomeDirectory, ".vscode", "extensions"); + const extensionsDir = path.join(yield* HostProcess.HomeDirectory, ".vscode", "extensions"); const defaultPath = yield* fileSystem .exists(extensionsDir) .pipe(Effect.orElseSucceed(() => false)); diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 869961f955b7..806b038d7696 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -18,7 +18,7 @@ import * as Option from "effect/Option"; import * as Electron from "electron"; import * as NetService from "@t3tools/shared/Net"; -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import type { RemoteT3RunnerOptions } from "@t3tools/ssh/tunnel"; import serverPackageJson from "../../server/package.json" with { type: "json" }; @@ -90,8 +90,8 @@ const layerDesktopEnvironment = Layer.unwrap( const metadata = yield* Effect.service(ElectronApp.ElectronApp).pipe( Effect.flatMap((app) => app.metadata), ); - const platform = yield* HostProcessPlatform; - const processArch = yield* HostProcessArchitecture; + const platform = yield* HostProcess.Platform; + const processArch = yield* HostProcess.Architecture; return DesktopEnvironment.layer({ dirname: __dirname, homeDirectory: NodeOS.homedir(), diff --git a/apps/desktop/src/preview/BrowserImport/BrowserImport.test.ts b/apps/desktop/src/preview/BrowserImport/BrowserImport.test.ts index 0f1e2bd386ce..edd4e981dee5 100644 --- a/apps/desktop/src/preview/BrowserImport/BrowserImport.test.ts +++ b/apps/desktop/src/preview/BrowserImport/BrowserImport.test.ts @@ -1,10 +1,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; -import { - HostProcessEnvironment, - HostProcessExecutablePath, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Fiber from "effect/Fiber"; @@ -52,10 +48,10 @@ const layerRejectedBeforeSession = Layer.succeed( const withImporter = Effect.fnUntraced(function* () { const fileSystem = yield* FileSystem.FileSystem; const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-import-" }); - const layerEnvironment = Layer.succeed(HostProcessEnvironment, { HOME: home }); + const layerEnvironment = Layer.succeed(HostProcess.Environment, { HOME: home }); const context = yield* sourcePathContext.pipe( - Effect.provideService(HostProcessEnvironment, { HOME: home }), - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Environment, { HOME: home }), + Effect.provideService(HostProcess.Platform, "darwin"), ); const root = helium.userDataDirectory(context); if (root === undefined) throw new Error("Helium has no macOS user-data directory"); @@ -69,8 +65,8 @@ const withImporter = Effect.fnUntraced(function* () { BrowserImport.layer.pipe( Layer.provide(layerRejectedBeforeSession), Layer.provide(layerEnvironment), - Layer.provide(Layer.succeed(HostProcessPlatform, "darwin")), - Layer.provide(Layer.succeed(HostProcessExecutablePath, "/Applications/T3 Code.app")), + Layer.provide(Layer.succeed(HostProcess.Platform, "darwin")), + Layer.provide(Layer.succeed(HostProcess.ExecutablePath, "/Applications/T3 Code.app")), Layer.provide(NodeServices.layer), ), ), diff --git a/apps/desktop/src/preview/BrowserImport/BrowserImport.ts b/apps/desktop/src/preview/BrowserImport/BrowserImport.ts index 8893f2322410..69fccc5b761d 100644 --- a/apps/desktop/src/preview/BrowserImport/BrowserImport.ts +++ b/apps/desktop/src/preview/BrowserImport/BrowserImport.ts @@ -21,7 +21,7 @@ import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; import { ChildProcessSpawner } from "effect/process"; -import { HostProcessExecutablePath, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as BrowserSession from "../BrowserSession.ts"; import { ChromiumCookieReadError, readChromiumCookies } from "./ChromiumCookies.ts"; @@ -169,8 +169,8 @@ export const writeCookies = Effect.fn("BrowserImport.writeCookies")(function* ( /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* BrowserImportMake() { const browserSession = yield* BrowserSession.BrowserSession; - const platform = yield* HostProcessPlatform; - const executablePath = yield* HostProcessExecutablePath; + const platform = yield* HostProcess.Platform; + const executablePath = yield* HostProcess.ExecutablePath; // Captured here so the service's methods stay free of a requirements // channel: the layer is built where NodeServices is already in scope. const platformServices = yield* Effect.context< diff --git a/apps/desktop/src/preview/BrowserImport/ChromiumCookies.ts b/apps/desktop/src/preview/BrowserImport/ChromiumCookies.ts index 2c2fd425df56..d524bf0938fc 100644 --- a/apps/desktop/src/preview/BrowserImport/ChromiumCookies.ts +++ b/apps/desktop/src/preview/BrowserImport/ChromiumCookies.ts @@ -315,7 +315,7 @@ export interface ChromiumCookieSource { readonly keychainAccount: string | undefined; readonly linuxSecretApplication: string | undefined; readonly windowsLocalStatePath?: string; - /** Supplied by the caller from `HostProcessPlatform` rather than read here. */ + /** Supplied by the caller from `HostProcess.Platform` rather than read here. */ readonly platform: NodeJS.Platform; } diff --git a/apps/desktop/src/preview/BrowserImport/ChromiumKeys.test.ts b/apps/desktop/src/preview/BrowserImport/ChromiumKeys.test.ts index 0466d6b4749c..cdeb67e0e8cc 100644 --- a/apps/desktop/src/preview/BrowserImport/ChromiumKeys.test.ts +++ b/apps/desktop/src/preview/BrowserImport/ChromiumKeys.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "@effect/vitest"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -232,7 +232,7 @@ describe("Linux Chromium secrets", () => { capture: (value) => (captured = value), }), ), - Effect.provideService(HostProcessEnvironment, { + Effect.provideService(HostProcess.Environment, { PATH: "/synthetic/bin", SESSION_MARKER: "kept", LC_ALL: "localized", @@ -322,7 +322,7 @@ describe("Windows Chromium secrets", () => { Effect.provide( layerHelper({ stdout: key.toString("base64"), capture: (value) => (captured = value) }), ), - Effect.provideService(HostProcessEnvironment, { SystemRoot: "C:\\Windows" }), + Effect.provideService(HostProcess.Environment, { SystemRoot: "C:\\Windows" }), ); }); }); diff --git a/apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts b/apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts index 35e1e9c4b55e..ef7c8685b707 100644 --- a/apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts +++ b/apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts @@ -19,7 +19,7 @@ */ import * as NodeCrypto from "node:crypto"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as Base64 from "effect/encoding/Base64"; @@ -138,7 +138,7 @@ export const readLinuxSecret = Effect.fn("ChromiumKeys.readLinuxSecret")(functio return yield* Effect.scoped( Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const environment = yield* HostProcessEnvironment; + const environment = yield* HostProcess.Environment; const helper = yield* LinuxBrowserSecretPath; if (helper === undefined) { return yield* new ChromiumKeyError({ reason: "keychainUnavailable" }); @@ -219,7 +219,7 @@ export const decodeWindowsWrappedKey = Effect.fn("ChromiumKeys.decodeWindowsWrap export const unwrapWindowsDpapiKey = Effect.fn("ChromiumKeys.unwrapWindowsDpapiKey")(function* ( wrapped: Buffer, ) { - const environment = yield* HostProcessEnvironment; + const environment = yield* HostProcess.Environment; return yield* Effect.scoped( Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; diff --git a/apps/desktop/src/preview/BrowserImport/SafariPermission.test.ts b/apps/desktop/src/preview/BrowserImport/SafariPermission.test.ts index cd41673a3f14..3b33f872d124 100644 --- a/apps/desktop/src/preview/BrowserImport/SafariPermission.test.ts +++ b/apps/desktop/src/preview/BrowserImport/SafariPermission.test.ts @@ -1,6 +1,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; -import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as PlatformError from "effect/PlatformError"; @@ -30,8 +30,8 @@ it.effect("detects Safari access becoming available without reading or importing ), }); const check = yield* safariPermissionCheck.pipe( - Effect.provideService(HostProcessEnvironment, { HOME: home }), - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Environment, { HOME: home }), + Effect.provideService(HostProcess.Platform, "darwin"), Effect.provideService(FileSystem.FileSystem, guardedFs), ); assert.isFalse(yield* Effect.promise(check)); @@ -47,8 +47,8 @@ it.effect("recognizes access when cookies exist only in a named Safari profile", const fs = yield* FileSystem.FileSystem; const home = yield* fs.makeTempDirectoryScoped({ prefix: "t3-safari-named-access-" }); const check = yield* safariPermissionCheck.pipe( - Effect.provideService(HostProcessEnvironment, { HOME: home }), - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Environment, { HOME: home }), + Effect.provideService(HostProcess.Platform, "darwin"), ); assert.isFalse(yield* Effect.promise(check)); const directory = `${home}/Library/Containers/com.apple.Safari/Data/Library/WebKit/WebsiteDataStore/12345678-1234-1234-1234-123456789abc/Cookies`; diff --git a/apps/desktop/src/preview/BrowserImport/Sources.test.ts b/apps/desktop/src/preview/BrowserImport/Sources.test.ts index 4a04e0498cb6..51bab02ce939 100644 --- a/apps/desktop/src/preview/BrowserImport/Sources.test.ts +++ b/apps/desktop/src/preview/BrowserImport/Sources.test.ts @@ -3,11 +3,7 @@ import * as NodePath from "node:path"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; -import { - HostProcessEnvironment, - HostProcessHostname, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; @@ -80,8 +76,8 @@ const withSourceHome = Effect.fnUntraced(function* () { const fileSystem = yield* FileSystem.FileSystem; const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-sources-" }); const context = yield* sourcePathContext.pipe( - Effect.provideService(HostProcessEnvironment, { HOME: home }), - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Environment, { HOME: home }), + Effect.provideService(HostProcess.Platform, "darwin"), ); yield* fileSystem.makeDirectory(userDataDirectory(context), { recursive: true }); return context; @@ -133,8 +129,8 @@ describe("Helium on Linux", () => { const fileSystem = yield* FileSystem.FileSystem; const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-helium-linux-" }); const context = yield* sourcePathContext.pipe( - Effect.provideService(HostProcessEnvironment, { HOME: home }), - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Environment, { HOME: home }), + Effect.provideService(HostProcess.Platform, "linux"), ); const root = `${home}/.config/net.imput.helium`; yield* fileSystem.makeDirectory(`${root}/Default`, { recursive: true }); @@ -162,11 +158,11 @@ describe("Helium on Windows", () => { run( Effect.gen(function* () { const context = yield* sourcePathContext.pipe( - Effect.provideService(HostProcessEnvironment, { + Effect.provideService(HostProcess.Environment, { USERPROFILE: "C:\\Users\\browser-user", LOCALAPPDATA: "C:\\Users\\browser-user\\AppData\\Local", }), - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), ); assert.include(helium.platforms, "win32"); @@ -198,11 +194,11 @@ describe("isSourceRunning", () => { prefix: "t3code-helium-windows-lock-", }); const context = yield* sourcePathContext.pipe( - Effect.provideService(HostProcessEnvironment, { + Effect.provideService(HostProcess.Environment, { HOME: home, LOCALAPPDATA: home, }), - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), ); const profile = context.path.join(helium.userDataDirectory(context)!, "Default"); const database = context.path.join(profile, "Network", "Cookies"); @@ -259,12 +255,12 @@ describe("isSourceRunning", () => { assert.isTrue( yield* isSourceRunning(helium, paths).pipe( - Effect.provideService(HostProcessHostname, "another-host"), + Effect.provideService(HostProcess.Hostname, "another-host"), ), ); assert.isFalse( yield* isSourceRunning(helium, paths).pipe( - Effect.provideService(HostProcessHostname, "lock-owner"), + Effect.provideService(HostProcess.Hostname, "lock-owner"), ), ); }), @@ -656,8 +652,8 @@ describe("cookieDatabaseCandidatePaths", () => { Effect.gen(function* () { const path = yield* Path.Path; const context = yield* sourcePathContext.pipe( - Effect.provideService(HostProcessEnvironment, { HOME: "/tmp/test" }), - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Environment, { HOME: "/tmp/test" }), + Effect.provideService(HostProcess.Platform, "darwin"), ); const candidates = cookieDatabaseCandidatePaths(firefox, context, "Profiles/abc.default"); assert.deepEqual(candidates, [ @@ -684,8 +680,8 @@ describe("Firefox Snap profiles", () => { prefix: "t3code-firefox-snap-", }); const context = yield* sourcePathContext.pipe( - Effect.provideService(HostProcessEnvironment, { HOME: home }), - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Environment, { HOME: home }), + Effect.provideService(HostProcess.Platform, "linux"), ); const root = context.path.join(home, "snap", "firefox", "common", ".mozilla", "firefox"); const directory = context.path.join(root, "abcd.default"); @@ -724,8 +720,8 @@ describe("Firefox Snap profiles", () => { const fileSystem = yield* FileSystem.FileSystem; const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-firefox-snap-" }); const context = yield* sourcePathContext.pipe( - Effect.provideService(HostProcessEnvironment, { HOME: home }), - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Environment, { HOME: home }), + Effect.provideService(HostProcess.Platform, "linux"), ); const native = context.path.join(home, ".mozilla", "firefox"); const snap = context.path.join(home, "snap", "firefox", "common", ".mozilla", "firefox"); @@ -775,11 +771,11 @@ describe("listSourceProfiles Firefox fallback", () => { }); const appData = path.join(home, "AppData", "Roaming"); const context = yield* sourcePathContext.pipe( - Effect.provideService(HostProcessEnvironment, { + Effect.provideService(HostProcess.Environment, { HOME: home, APPDATA: appData, }), - Effect.provideService(HostProcessPlatform, platform), + Effect.provideService(HostProcess.Platform, platform), ); const root = firefox.userDataDirectory(context)!; const scanRoot = platform === "linux" ? root : path.join(root, "Profiles"); @@ -812,8 +808,8 @@ describe("listSourceProfiles Firefox fallback", () => { prefix: "t3code-firefox-stale-ini-", }); const context = yield* sourcePathContext.pipe( - Effect.provideService(HostProcessEnvironment, { HOME: home }), - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Environment, { HOME: home }), + Effect.provideService(HostProcess.Platform, "darwin"), ); const root = firefox.userDataDirectory(context)!; // `profiles.ini` names a profile that was never launched (no cookie @@ -851,8 +847,8 @@ describe("listSourceProfiles Firefox fallback", () => { prefix: "t3code-firefox-counts-", }); const context = yield* sourcePathContext.pipe( - Effect.provideService(HostProcessEnvironment, { HOME: home }), - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Environment, { HOME: home }), + Effect.provideService(HostProcess.Platform, "darwin"), ); const root = firefox.userDataDirectory(context)!; const declaredDirectory = path.join(root, "Profiles", "declared.default"); @@ -902,8 +898,8 @@ describe("isSourceRunning for Firefox", () => { const fileSystem = yield* FileSystem.FileSystem; const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-firefox-" }); const context = yield* sourcePathContext.pipe( - Effect.provideService(HostProcessEnvironment, { HOME: home }), - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Environment, { HOME: home }), + Effect.provideService(HostProcess.Platform, "darwin"), ); const root = firefox.userDataDirectory(context)!; const profile = `${root}/Profiles/abcd.default-release`; @@ -952,7 +948,7 @@ describe("isSourceRunning for Firefox", () => { ); // Holds the lock with python3's fcntl, which does not exist on Windows. - it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")( + it.effect.skipIf(HostProcess.Platform.defaultValue() === "win32")( "detects a live fcntl lock on .parentlock, as macOS Firefox leaves it", () => run( @@ -961,8 +957,8 @@ describe("isSourceRunning for Firefox", () => { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-firefox-" }); const context = yield* sourcePathContext.pipe( - Effect.provideService(HostProcessEnvironment, { HOME: home }), - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Environment, { HOME: home }), + Effect.provideService(HostProcess.Platform, "darwin"), ); const root = firefox.userDataDirectory(context)!; const profile = `${root}/Profiles/abcd.default-release`; @@ -1033,11 +1029,11 @@ describe("isSourceRunning for Firefox", () => { const context = yield* sourcePathContext.pipe( // Firefox's win32 root hangs off %APPDATA%; without it the root is // undefined and the fixture would escape the sandbox into the repo. - Effect.provideService(HostProcessEnvironment, { + Effect.provideService(HostProcess.Environment, { HOME: home, APPDATA: `${home}/AppData/Roaming`, }), - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), ); const root = firefox.userDataDirectory(context)!; const profile = `${root}/Profiles/gx7x7fqx.default-release`; diff --git a/apps/desktop/src/preview/BrowserImport/Sources.ts b/apps/desktop/src/preview/BrowserImport/Sources.ts index fa8f79e1ccba..86baffbf1b2f 100644 --- a/apps/desktop/src/preview/BrowserImport/Sources.ts +++ b/apps/desktop/src/preview/BrowserImport/Sources.ts @@ -17,12 +17,7 @@ */ import type { BrowserImportSourceId, BrowserImportSourceProfile } from "@t3tools/contracts"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; -import { - HostProcessEnvironment, - HostProcessAddresses, - HostProcessHostname, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; @@ -335,8 +330,8 @@ export function parseFirefoxProfiles( */ export const sourcePathContext = Effect.gen(function* () { const path = yield* Path.Path; - const platform = yield* HostProcessPlatform; - const environment = yield* HostProcessEnvironment; + const platform = yield* HostProcess.Platform; + const environment = yield* HostProcess.Environment; return { path, platform, @@ -782,7 +777,7 @@ export const posixLockIsHeld = Effect.fnUntraced(function* ( interpreters: ReadonlyArray = FCNTL_PROBE_INTERPRETERS, ) { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const environment = yield* HostProcessEnvironment; + const environment = yield* HostProcess.Environment; for (const interpreter of interpreters) { const verdict = yield* Effect.scoped( Effect.gen(function* () { @@ -872,7 +867,7 @@ export const isSourceRunning = Effect.fn("BrowserImportSources.isSourceRunning") if (context.platform === "win32") { return yield* windowsChromiumCookiesAreHeld(definition, context); } - const currentHost = yield* HostProcessHostname; + const currentHost = yield* HostProcess.Hostname; const lock = context.path.join(root, "SingletonLock"); return yield* fileSystem.readLink(lock).pipe( Effect.flatMap((target) => chromiumSingletonLockIsHeld(target, currentHost, processIsAlive)), @@ -883,7 +878,7 @@ export const isSourceRunning = Effect.fn("BrowserImportSources.isSourceRunning") const profiles = yield* listSourceProfiles(definition, context); // Only the Linux `lock` symlink names an address, so Windows skips the lookup. const localAddresses: ReadonlySet = - context.platform === "win32" ? new Set() : yield* yield* HostProcessAddresses; + context.platform === "win32" ? new Set() : yield* yield* HostProcess.Addresses; const found = yield* Effect.forEach(profiles, (profile) => { const directory = context.path.isAbsolute(profile.directory) ? profile.directory diff --git a/apps/desktop/src/preview/Manager.test.ts b/apps/desktop/src/preview/Manager.test.ts index c13e2ce2868d..6d585a88641c 100644 --- a/apps/desktop/src/preview/Manager.test.ts +++ b/apps/desktop/src/preview/Manager.test.ts @@ -5,7 +5,7 @@ import type { DesktopPreviewRecordingFrame, DesktopPreviewRecordingInputEvent, } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { parseKeybindingShortcut } from "@t3tools/shared/keybindings"; import * as Cause from "effect/Cause"; import * as Deferred from "effect/Deferred"; @@ -313,7 +313,7 @@ const managerLayer = (platform: NodeJS.Platform = "darwin") => Layer.provideMerge(layerFileSystem), Layer.provideMerge(Path.layer), Layer.provideMerge(NodeCrypto.layer), - Layer.provideMerge(Layer.succeed(HostProcessPlatform, platform)), + Layer.provideMerge(Layer.succeed(HostProcess.Platform, platform)), ); const withManager = ( diff --git a/apps/desktop/src/preview/Manager.ts b/apps/desktop/src/preview/Manager.ts index 58a7413ede2c..aa36055452eb 100644 --- a/apps/desktop/src/preview/Manager.ts +++ b/apps/desktop/src/preview/Manager.ts @@ -25,7 +25,7 @@ import type { DesktopPreviewTabDefaults, PreviewForwardedShortcut, } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { matchesKeybindingShortcut } from "@t3tools/shared/keybindings"; import { normalizePreviewUrl } from "@t3tools/shared/preview"; import { @@ -544,7 +544,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function ) { const fileSystem = yield* FileSystem.FileSystem; const rendererHistory = yield* DesktopRendererHistory.DesktopRendererHistory; - const hostPlatform = yield* HostProcessPlatform; + const hostPlatform = yield* HostProcess.Platform; const path = yield* Path.Path; const crypto = yield* Crypto.Crypto; const parentScope = yield* Scope.Scope; diff --git a/apps/desktop/src/snapShot/DesktopSnapShot.ts b/apps/desktop/src/snapShot/DesktopSnapShot.ts index 5718676f3b7a..71fe47d3f6ca 100644 --- a/apps/desktop/src/snapShot/DesktopSnapShot.ts +++ b/apps/desktop/src/snapShot/DesktopSnapShot.ts @@ -88,7 +88,7 @@ import { snapShotShortcutRegistrationFailureMessage, snapShotShortcutSystemConflict, } from "./snapShot.ts"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; const MAX_CAPTURE_WIDTH = 2_560; const MAX_CAPTURE_HEIGHT = 1_600; @@ -728,7 +728,7 @@ export const make = Effect.gen(function* () { const runPromise = Effect.runPromiseWith(context); const captureDirectory = path.join(environment.stateDir, "snap-shots"); const linuxAppId = environment.linuxDesktopEntryName.replace(/\.desktop$/, ""); - const home = yield* HostProcessHomeDirectory; + const home = yield* HostProcess.HomeDirectory; let shortcutVerified = false; const gnomeSetupPaths = { bundle: environment.isPackaged diff --git a/apps/desktop/src/snapShot/snapShot.test.ts b/apps/desktop/src/snapShot/snapShot.test.ts index 634c4653a5a6..ea52e877b93f 100644 --- a/apps/desktop/src/snapShot/snapShot.test.ts +++ b/apps/desktop/src/snapShot/snapShot.test.ts @@ -1,5 +1,5 @@ import { it as effectIt } from "@effect/vitest"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import { describe, expect, it, vi } from "vite-plus/test"; @@ -703,7 +703,7 @@ describe("isWaylandSession", () => { "falls back to a live runtime directory socket when session variables are stripped", () => Effect.gen(function* () { - if ((yield* HostProcessPlatform) !== "linux") return; + if ((yield* HostProcess.Platform) !== "linux") return; yield* Effect.promise(async () => { const { mkdtemp, rm, writeFile } = await import("node:fs/promises"); const { createServer } = await import("node:net"); diff --git a/apps/server/scripts/record-codex-app-server-replay-fixture.ts b/apps/server/scripts/record-codex-app-server-replay-fixture.ts index 4343aec87ba1..0d18e917e031 100644 --- a/apps/server/scripts/record-codex-app-server-replay-fixture.ts +++ b/apps/server/scripts/record-codex-app-server-replay-fixture.ts @@ -5,7 +5,7 @@ import { revertCodexThread } from "../src/provider/CodexThreadRevert.ts"; import { buildCodexInitializeParams } from "../src/provider/CodexProvider.ts"; import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; import * as NodeServices from "@effect/platform-node/NodeServices"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; import * as Console from "effect/Console"; import * as Deferred from "effect/Deferred"; @@ -1211,7 +1211,8 @@ function layerCodex({ recorder }: { readonly recorder: Recorder }) { Effect.gen(function* () { // The vp node shim marks its children with VP_TOOL_RECURSION, which makes every // `node` the agent runs through a shell fail. Codex must not inherit it. - const { VP_TOOL_RECURSION: _vpToolRecursion, ...environment } = yield* HostProcessEnvironment; + const { VP_TOOL_RECURSION: _vpToolRecursion, ...environment } = + yield* HostProcess.Environment; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const commandName = environment.T3_CODEX_BIN ?? environment.CODEX_BIN ?? "codex"; const spawnCommand = yield* resolveSpawnCommand(commandName, ["app-server"], { diff --git a/apps/server/scripts/record-grok-acp-replay-fixture.ts b/apps/server/scripts/record-grok-acp-replay-fixture.ts index f04b04cfe348..6415a145690d 100644 --- a/apps/server/scripts/record-grok-acp-replay-fixture.ts +++ b/apps/server/scripts/record-grok-acp-replay-fixture.ts @@ -12,7 +12,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { type ProviderReplayEntry } from "@t3tools/contracts"; import { GrokSettings } from "@t3tools/provider-grok/settings"; -import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime"; import * as Clock from "effect/Clock"; import * as Console from "effect/Console"; @@ -442,12 +442,12 @@ const recordScenario = Effect.fn("recordGrokScenario")(function* (fixtureName: s const settings = { ...DEFAULT_GROK_SETTINGS, binaryPath: process.env.T3_GROK_BIN ?? "grok" }; const layerRegistry = ProviderAdapterRegistry.layerFromAdaptersEffect( Effect.gen(function* () { - const environment = yield* HostProcessEnvironment; + const environment = yield* HostProcess.Environment; const adapter = yield* makeGrokAdapterV2({ instanceId: GROK_DEFAULT_INSTANCE_ID, settings, environment, - hostPlatform: yield* HostProcessPlatform, + hostPlatform: yield* HostProcess.Platform, selfInvocation: yield* resolveSelfInvocation(), continuationRequests: yield* ProviderContinuationRequests.ProviderContinuationRequests, // Production's runtime factory, with the protocol logger teeing raw lines. diff --git a/apps/server/scripts/t3-sqlite-state.test.ts b/apps/server/scripts/t3-sqlite-state.test.ts index 45bc78b30e0d..4946d1985639 100644 --- a/apps/server/scripts/t3-sqlite-state.test.ts +++ b/apps/server/scripts/t3-sqlite-state.test.ts @@ -7,7 +7,7 @@ import * as SqlClient from "effect/sql/SqlClient"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; import { runSqliteState } from "./t3-sqlite-state.ts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; const createFixtureDatabase = Effect.fn("createSqliteStateFixtureDatabase")(function* ( @@ -90,7 +90,7 @@ it.layer(NodeServices.layer)("t3-sqlite-state", (it) => { sql: "INSERT INTO fixtures (id, label) VALUES (2, 'seeded')", }); assert.equal(mutation.operation, "exec"); - if (mutation.operation === "exec" && (yield* HostProcessPlatform) !== "win32") { + if (mutation.operation === "exec" && (yield* HostProcess.Platform) !== "win32") { // NTFS has no POSIX mode bits to report. assert.equal((yield* fs.stat(mutation.backup)).mode & 0o777, 0o600); } diff --git a/apps/server/src/assets/AssetAccess.test.ts b/apps/server/src/assets/AssetAccess.test.ts index 6b6d4b2b95da..8a92b40aaea2 100644 --- a/apps/server/src/assets/AssetAccess.test.ts +++ b/apps/server/src/assets/AssetAccess.test.ts @@ -36,7 +36,7 @@ import { openMediaFile } from "./MediaFile.ts"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; import * as GitHubCredentials from "@t3tools/source-control-github/server/GitHubCredentials"; import { githubMediaResponse } from "./GitHubMediaFetch.ts"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; vi.mock("node:fs/promises", async (importOriginal) => { const actual = await importOriginal(); @@ -326,7 +326,7 @@ describe("AssetAccess", () => { expect(yield* Effect.promise(() => response.text())).toBe("recording bytes"); } } - }).pipe(Effect.provideService(HostProcessHomeDirectory, home)); + }).pipe(Effect.provideService(HostProcess.HomeDirectory, home)); }).pipe(Effect.provide(layerTest)), ); diff --git a/apps/server/src/assets/AssetAccess.ts b/apps/server/src/assets/AssetAccess.ts index d2de3e555736..43d7d75f7864 100644 --- a/apps/server/src/assets/AssetAccess.ts +++ b/apps/server/src/assets/AssetAccess.ts @@ -58,7 +58,7 @@ import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts"; import * as WorkspacePaths from "../workspace/WorkspacePaths.ts"; import * as NativeAppIconResolver from "./NativeAppIconResolver.ts"; import { openMediaFile, readMediaFileHeader, type OpenMediaFile } from "./MediaFile.ts"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; export const ASSET_ROUTE_PREFIX = "/api/assets"; @@ -470,7 +470,7 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i switch (input.resource._tag) { case "media-file": { - let requestedPath = expandHomePath(input.resource.path, yield* HostProcessHomeDirectory); + let requestedPath = expandHomePath(input.resource.path, yield* HostProcess.HomeDirectory); if (!path.isAbsolute(requestedPath)) { if (!input.workspaceRoot) { return yield* new AssetWorkspaceContextNotFoundError({ resource: input.resource }); diff --git a/apps/server/src/assets/NativeAppIconResolver.test.ts b/apps/server/src/assets/NativeAppIconResolver.test.ts index 753f7378285d..6c69eebceafc 100644 --- a/apps/server/src/assets/NativeAppIconResolver.test.ts +++ b/apps/server/src/assets/NativeAppIconResolver.test.ts @@ -5,7 +5,7 @@ import * as Layer from "effect/Layer"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import { ChildProcessSpawner } from "effect/process"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as ServerConfig from "../config.ts"; import * as NativeAppIconResolver from "./NativeAppIconResolver.ts"; @@ -44,7 +44,7 @@ describe("resolveNativeAppIcon", () => { }); const layerDependencies = Layer.mergeAll( layerConfig, - Layer.succeed(HostProcessPlatform, "darwin"), + Layer.succeed(HostProcess.Platform, "darwin"), Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner), ).pipe(Layer.provideMerge(NodeServices.layer)); const layerTest = NativeAppIconResolver.layer.pipe(Layer.provide(layerDependencies)); diff --git a/apps/server/src/assets/NativeAppIconResolver.ts b/apps/server/src/assets/NativeAppIconResolver.ts index df5c8a03f670..e25610e5223a 100644 --- a/apps/server/src/assets/NativeAppIconResolver.ts +++ b/apps/server/src/assets/NativeAppIconResolver.ts @@ -15,7 +15,7 @@ import * as PlatformError from "effect/PlatformError"; import * as Semaphore from "effect/Semaphore"; import * as Hex from "effect/encoding/Hex"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as ServerConfig from "../config.ts"; @@ -214,7 +214,7 @@ const resolveNativeAppIconUncached = Effect.fn("NativeAppIconResolver.resolveUnc /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; - const hostPlatform = yield* HostProcessPlatform; + const hostPlatform = yield* HostProcess.Platform; const resolutionSemaphore = yield* Semaphore.make(2); const resolutionCache: Cache.Cache< string, diff --git a/apps/server/src/bootstrap.test.ts b/apps/server/src/bootstrap.test.ts index d532f71c9fb8..0b14c7dd1e23 100644 --- a/apps/server/src/bootstrap.test.ts +++ b/apps/server/src/bootstrap.test.ts @@ -11,7 +11,7 @@ import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; import * as TestClock from "effect/testing/TestClock"; import { vi } from "vite-plus/test"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { BootstrapEnvelopeDecodeError, @@ -55,7 +55,7 @@ vi.mock("node:fs", async (importOriginal) => { }; }); -const windowsHost = HostProcessPlatform.defaultValue() === "win32"; +const windowsHost = HostProcess.Platform.defaultValue() === "win32"; const nullDevice = windowsHost ? "\\\\.\\NUL" : "/dev/null"; const closeIfOpen = (fd: number) => { try { @@ -116,7 +116,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => { try { const payload = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { timeoutMs: 100, - }).pipe(Effect.provideService(HostProcessPlatform, "linux")); + }).pipe(Effect.provideService(HostProcess.Platform, "linux")); assertSome(payload, { mode: "desktop", }); @@ -141,7 +141,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => { try { const error = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { timeoutMs: 100, - }).pipe(Effect.provideService(HostProcessPlatform, "linux"), Effect.flip); + }).pipe(Effect.provideService(HostProcess.Platform, "linux"), Effect.flip); assert.instanceOf(error, BootstrapInputStreamOpenError); assert.equal(error.fd, fd); diff --git a/apps/server/src/bootstrap.ts b/apps/server/src/bootstrap.ts index 71dae0dc3096..708a8fd9119b 100644 --- a/apps/server/src/bootstrap.ts +++ b/apps/server/src/bootstrap.ts @@ -10,7 +10,7 @@ import * as Predicate from "effect/Predicate"; import * as Result from "effect/Result"; import * as Schema from "effect/Schema"; import { decodeJsonResult } from "@t3tools/shared/schemaJson"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; export class BootstrapFdStatError extends Schema.TaggedError()( "BootstrapFdStatError", @@ -168,7 +168,7 @@ const isFdReady = (fd: number) => const makeBootstrapInputStream = (fd: number) => Effect.gen(function* () { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const fdPath = resolveFdPath(fd, platform); return yield* Effect.try({ try: () => { diff --git a/apps/server/src/checkpointing/CheckpointStore.test.ts b/apps/server/src/checkpointing/CheckpointStore.test.ts index c1ad293d40ab..2846b86e1b88 100644 --- a/apps/server/src/checkpointing/CheckpointStore.test.ts +++ b/apps/server/src/checkpointing/CheckpointStore.test.ts @@ -4,7 +4,7 @@ import * as NodePath from "node:path"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { it } from "@effect/vitest"; import { ThreadId, type VcsError } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; @@ -329,7 +329,7 @@ it.layer(layerTest)("CheckpointStore.layer", (it) => { const copiedText = Array.from({ length: 20 }, (_, index) => `copy line ${index}\n`).join( "", ); - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const renamedPath = platform === "win32" ? "renamed café.txt" : "renamed\tcafé\nname.txt"; const addedPath = platform === "win32" ? "new café.txt" : "new\tfile\n名.txt"; for (const [path, contents] of Object.entries({ diff --git a/apps/server/src/cli/app.test.ts b/apps/server/src/cli/app.test.ts index 0c35ab9f6941..e785baa58f1c 100644 --- a/apps/server/src/cli/app.test.ts +++ b/apps/server/src/cli/app.test.ts @@ -8,12 +8,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { it } from "@effect/vitest"; import type { DesktopAppActivationRequest } from "@t3tools/contracts"; import { resolveDesktopAppControlAddress } from "@t3tools/shared/desktopAppControl"; -import { - HostProcessHomeDirectory, - HostProcessPlatform, - HostProcessUserId, - HostProcessWorkingDirectory, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as NetService from "@t3tools/shared/Net"; import * as ConfigProvider from "effect/ConfigProvider"; import * as Effect from "effect/Effect"; @@ -110,8 +105,8 @@ async function startFakeDesktop(input: { const fakeDesktop = Effect.fn(function* ( input: Omit[0], "platform" | "userId">, ) { - const platform = yield* HostProcessPlatform; - const userId = yield* HostProcessUserId; + const platform = yield* HostProcess.Platform; + const userId = yield* HostProcess.UserId; return yield* Effect.acquireRelease( Effect.promise(() => startFakeDesktop({ ...input, platform, userId })), (server) => Effect.promise(() => server.close()), @@ -142,7 +137,7 @@ describe("t3 server command safety", () => { "C:new-project", ]) { const error = yield* runCli([word, "--base-dir", baseDir]).pipe( - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), Effect.flip, ); expect(String(error)).toContain(`Unknown command "${word}"`); @@ -181,7 +176,7 @@ describe("t3 server command safety", () => { yield* Effect.promise(() => NodeFSP.mkdir(stateDir, { recursive: true })); yield* Effect.promise(() => NodeFSP.writeFile(statePath, record)); const newDirectory = NodePath.join(root, "new-project"); - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; for (const args of [ [], ["start"], @@ -193,7 +188,7 @@ describe("t3 server command safety", () => { ]) { const error = yield* runCli(args, { T3CODE_HOME: baseDir }).pipe( Effect.provideService( - HostProcessPlatform, + HostProcess.Platform, args[0] === "C:new-project" ? "win32" : platform, ), Effect.flip, @@ -234,7 +229,7 @@ describe("t3 app", () => { Effect.gen(function* () { const baseDir = NodePath.join(root, "missing-t3-home"); const error = yield* runCli(["app", "--base-dir", baseDir]).pipe( - Effect.provideService(HostProcessPlatform, "freebsd"), + Effect.provideService(HostProcess.Platform, "freebsd"), Effect.flip, ); @@ -257,7 +252,7 @@ describe("t3 app", () => { expect(error).toMatchObject({ _tag: "DesktopAppUnreachableError", candidateAddresses: [expect.any(String)], - workspaceRoot: yield* HostProcessWorkingDirectory, + workspaceRoot: yield* HostProcess.WorkingDirectory, message: expect.stringContaining("Could not reach the T3 Code desktop app."), cause: { code: "ENOENT" }, }); @@ -271,8 +266,8 @@ describe("t3 app", () => { Effect.gen(function* () { const baseDir = NodePath.join(root, "t3-home"); const explicitPath = NodePath.join(root, "project"); - const platform = yield* HostProcessPlatform; - const workingDirectory = yield* HostProcessWorkingDirectory; + const platform = yield* HostProcess.Platform; + const workingDirectory = yield* HostProcess.WorkingDirectory; const desktop = yield* fakeDesktop({ baseDir }); yield* runCli(["app"], { T3CODE_HOME: baseDir }); @@ -298,7 +293,7 @@ describe("t3 app", () => { expect(desktop.received).toHaveLength(1); expect(development.received).toHaveLength(0); - }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped), + }).pipe(Effect.provideService(HostProcess.HomeDirectory, root), Effect.scoped), ), ); @@ -313,7 +308,7 @@ describe("t3 app", () => { expect(development.received).toHaveLength(2); expect(yield* pathExists(baseDir)).toBe(false); - }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped), + }).pipe(Effect.provideService(HostProcess.HomeDirectory, root), Effect.scoped), ), ); @@ -329,7 +324,7 @@ describe("t3 app", () => { expect(flagError).toMatchObject({ _tag: "DesktopAppUnreachableError" }); expect(envError).toMatchObject({ _tag: "DesktopAppUnreachableError" }); expect(development.received).toHaveLength(0); - }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped), + }).pipe(Effect.provideService(HostProcess.HomeDirectory, root), Effect.scoped), ), ); @@ -363,7 +358,7 @@ describe("t3 app", () => { _tag: "DesktopAppRequestFailedError", code: "project-create-failed", requestId: desktop.received[0]?.requestId, - workspaceRoot: yield* HostProcessWorkingDirectory, + workspaceRoot: yield* HostProcess.WorkingDirectory, message: expect.stringContaining("project-create-failed"), cause: { ok: false, @@ -377,7 +372,7 @@ describe("t3 app", () => { cause: { message: "The desktop app response is invalid." }, }); } - }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped), + }).pipe(Effect.provideService(HostProcess.HomeDirectory, root), Effect.scoped), ), ); }); diff --git a/apps/server/src/cli/app.ts b/apps/server/src/cli/app.ts index d9ad57f5127d..eade5887c6c1 100644 --- a/apps/server/src/cli/app.ts +++ b/apps/server/src/cli/app.ts @@ -10,11 +10,7 @@ import { type DesktopAppActivationRequest, } from "@t3tools/contracts"; import { resolveDesktopAppControlAddress } from "@t3tools/shared/desktopAppControl"; -import { - HostProcessPlatform, - HostProcessUserId, - HostProcessWorkingDirectory, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Config from "effect/Config"; import * as Console from "effect/Console"; import * as Crypto from "effect/Crypto"; @@ -188,7 +184,7 @@ const runAppCommand = Effect.fn("cli.app")(function* (flags: { readonly workspaceRoot: Option.Option; }) { const environment = yield* appEnvironment; - const hostPlatform = yield* HostProcessPlatform; + const hostPlatform = yield* HostProcess.Platform; if (Option.isSome(environment.sshConnection) || Option.isSome(environment.sshTty)) { return yield* new DesktopAppSshUnsupportedError({}); } @@ -201,9 +197,9 @@ const runAppCommand = Effect.fn("cli.app")(function* (flags: { const baseDir = yield* resolveBaseDir(configuredBaseDir); const allowDevFallback = Option.isNone(flags.baseDir) && !environment.t3Home?.trim(); const rawWorkspaceRoot = - Option.getOrUndefined(flags.workspaceRoot) ?? (yield* HostProcessWorkingDirectory); + Option.getOrUndefined(flags.workspaceRoot) ?? (yield* HostProcess.WorkingDirectory); const workspaceRoot = path.resolve(yield* expandHomePath(rawWorkspaceRoot)); - const userId = yield* HostProcessUserId; + const userId = yield* HostProcess.UserId; const resolveAddress = (stateSubdirectory: "userdata" | "dev") => resolveDesktopAppControlAddress({ stateDir: path.join(baseDir, stateSubdirectory), diff --git a/apps/server/src/cli/browser.ts b/apps/server/src/cli/browser.ts index 12f74ba60d2d..9bff41cb4fed 100644 --- a/apps/server/src/cli/browser.ts +++ b/apps/server/src/cli/browser.ts @@ -10,11 +10,7 @@ * Both need root. Without it, the command prints what it would change and the * `sudo` line to run. It is safe to run again; it skips what is already done. */ -import { - HostProcessEnvironment, - HostProcessPlatform, - HostProcessUserId, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Console from "effect/Console"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -61,7 +57,7 @@ const runStep = Effect.fn("browserSetup.runStep")(function* ( * unspecified home falls back to the invoking user's `~/.t3`. */ const setupBaseDir = Effect.fn("browserSetup.baseDir")(function* (explicit: Option.Option) { - const env = yield* HostProcessEnvironment; + const env = yield* HostProcess.Environment; const raw = Option.getOrUndefined(explicit) ?? env.T3CODE_HOME; if (raw !== undefined || env.SUDO_USER === undefined) return yield* resolveBaseDir(raw); const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; @@ -109,11 +105,11 @@ const browserSetupCommand = Command.make("setup", { baseDir: baseDirFlag }).pipe ), Command.withHandler(({ baseDir }) => Effect.gen(function* () { - if ((yield* HostProcessPlatform) !== "linux") { + if ((yield* HostProcess.Platform) !== "linux") { return yield* Console.log("Nothing to set up: T3's browser runs as is on this system."); } const fs = yield* FileSystem.FileSystem; - const isRoot = (yield* HostProcessUserId) === 0; + const isRoot = (yield* HostProcess.UserId) === 0; const setupCommand = yield* resolveRootCliCommand(PreviewBrowserHost.SETUP_SUBCOMMAND); const needsProfile = yield* PreviewBrowserHost.sandboxBlocked; diff --git a/apps/server/src/cli/connect.ts b/apps/server/src/cli/connect.ts index 83d384097ecc..866cc5ec8927 100644 --- a/apps/server/src/cli/connect.ts +++ b/apps/server/src/cli/connect.ts @@ -5,7 +5,7 @@ import { type RelayClientInstallProgressStage, } from "@t3tools/contracts"; import { RelayOkResponse } from "@t3tools/contracts/relay"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as RelayClient from "@t3tools/shared/relayClient"; import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; import * as Cause from "effect/Cause"; @@ -686,7 +686,7 @@ export const connectCommand = Command.make("connect", { // show how to run the server manually. const background = yield* recoverServiceOnboardingOffer(offerServiceDuringOnboarding); if (background) { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; yield* Console.log( platform === "darwin" ? "\n✓ Background service ready\n\nT3 Code is set to run while you are logged in to this Mac. The server establishes the T3 Connect link on startup." diff --git a/apps/server/src/cli/invocation.test.ts b/apps/server/src/cli/invocation.test.ts index bf2dcba2b9f4..5cd89a1457b2 100644 --- a/apps/server/src/cli/invocation.test.ts +++ b/apps/server/src/cli/invocation.test.ts @@ -1,12 +1,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, expect, it } from "@effect/vitest"; -import { - HostProcessArguments, - HostProcessEnvironment, - HostProcessExecutablePath, - HostProcessIsExecutable, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; @@ -113,11 +107,11 @@ it.layer(NodeServices.layer)("root CLI commands", (it) => { readonly executable?: boolean; }) => resolveRootCliCommand("browser setup").pipe( - Effect.provideService(HostProcessExecutablePath, input.node), - Effect.provideService(HostProcessArguments, [input.node, input.entry]), - Effect.provideService(HostProcessIsExecutable, input.executable ?? false), - Effect.provideService(HostProcessPlatform, "linux"), - Effect.provideService(HostProcessEnvironment, { PATH: input.path ?? "", ...input.env }), + Effect.provideService(HostProcess.ExecutablePath, input.node), + Effect.provideService(HostProcess.Arguments, [input.node, input.entry]), + Effect.provideService(HostProcess.IsExecutable, input.executable ?? false), + Effect.provideService(HostProcess.Platform, "linux"), + Effect.provideService(HostProcess.Environment, { PATH: input.path ?? "", ...input.env }), ); /** A directory holding an executable `t3`, to stand in for one on PATH. */ @@ -201,9 +195,9 @@ it.layer(NodeServices.layer)("manual server installation ownership", (it) => { yield* fs.makeDirectory(path.dirname(entry), { recursive: true }); yield* fs.writeFileString(entry, ""); const installation = yield* resolveServerInstallation.pipe( - Effect.provideService(HostProcessArguments, ["node", entry]), - Effect.provideService(HostProcessExecutablePath, entry), - Effect.provideService(HostProcessIsExecutable, entry.endsWith("/t3")), + Effect.provideService(HostProcess.Arguments, ["node", entry]), + Effect.provideService(HostProcess.ExecutablePath, entry), + Effect.provideService(HostProcess.IsExecutable, entry.endsWith("/t3")), ); expect(installation).toEqual({ kind }); } @@ -227,9 +221,9 @@ it.layer(NodeServices.layer)("manual server installation ownership", (it) => { '{"name":"t3","version":"0.0.45","bin":{"t3":"./dist/bin.mjs"}}', ); const resolve = resolveServerInstallation.pipe( - Effect.provideService(HostProcessArguments, ["node", entry]), - Effect.provideService(HostProcessIsExecutable, false), - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Arguments, ["node", entry]), + Effect.provideService(HostProcess.IsExecutable, false), + Effect.provideService(HostProcess.Platform, "linux"), ); expect(yield* resolve).toBeNull(); yield* fs.symlink(entry, globalBin); @@ -237,7 +231,7 @@ it.layer(NodeServices.layer)("manual server installation ownership", (it) => { yield* fs.remove(globalBin); yield* fs.writeFileString(globalBin, "an unrelated t3 command"); expect(yield* resolve).toBeNull(); - expect(yield* resolve.pipe(Effect.provideService(HostProcessPlatform, "win32"))).toBeNull(); + expect(yield* resolve.pipe(Effect.provideService(HostProcess.Platform, "win32"))).toBeNull(); }), ); @@ -261,9 +255,9 @@ it.layer(NodeServices.layer)("manual server installation ownership", (it) => { ); yield* fs.symlink(launcher, path.join(prefix, "bin/t3")); const resolve = resolveServerInstallation.pipe( - Effect.provideService(HostProcessExecutablePath, entry), - Effect.provideService(HostProcessIsExecutable, true), - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.ExecutablePath, entry), + Effect.provideService(HostProcess.IsExecutable, true), + Effect.provideService(HostProcess.Platform, "linux"), ); for (const [version, expected] of [ ["0.0.44", null], @@ -296,8 +290,8 @@ it.layer(NodeServices.layer)("manual server installation ownership", (it) => { } expect( yield* resolveServerInstallation.pipe( - Effect.provideService(HostProcessArguments, ["node", entry]), - Effect.provideService(HostProcessIsExecutable, false), + Effect.provideService(HostProcess.Arguments, ["node", entry]), + Effect.provideService(HostProcess.IsExecutable, false), ), ).toBeNull(); } diff --git a/apps/server/src/cli/invocation.ts b/apps/server/src/cli/invocation.ts index 59843abd7289..ff5cb9d8f9c8 100644 --- a/apps/server/src/cli/invocation.ts +++ b/apps/server/src/cli/invocation.ts @@ -5,13 +5,7 @@ import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import type { ServerInstallation } from "@t3tools/contracts"; -import { - HostProcessArguments, - HostProcessEnvironment, - HostProcessExecutablePath, - HostProcessIsExecutable, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { isCommandAvailable } from "@t3tools/shared/shell"; import packageJson from "../../package.json" with { type: "json" }; @@ -60,10 +54,10 @@ const decodeInstallManifest = Schema.decodeUnknownEffect(Schema.fromJsonString(I export const resolveServerInstallation = Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const args = yield* HostProcessArguments; - const executable = yield* HostProcessIsExecutable; - const executablePath = yield* HostProcessExecutablePath; - const platform = yield* HostProcessPlatform; + const args = yield* HostProcess.Arguments; + const executable = yield* HostProcess.IsExecutable; + const executablePath = yield* HostProcess.ExecutablePath; + const platform = yield* HostProcess.Platform; const entry = yield* fs.realPath(executable ? executablePath : (args[1] ?? "")); const match = /^(.*)\/lib\/node_modules\/t3\/(?:dist\/bin\.mjs|bin\/t3\.js|node_modules\/@t3code\/t3-[^/]+\/t3)$/.exec( @@ -141,7 +135,7 @@ export function formatCliCommand(input: { /** `formatCliCommand` against this process's real entry path and version. */ export const resolveCliCommand = (subcommand: string) => - Effect.map(HostProcessArguments, (processArguments) => + Effect.map(HostProcess.Arguments, (processArguments) => formatCliCommand({ subcommand, entryPath: processArguments[1] ?? "", @@ -161,12 +155,12 @@ const shellWord = (value: string) => */ const resolveInstallLauncher = Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; - const shim = (yield* HostProcessEnvironment).T3CODE_CLI_PATH?.trim(); + const shim = (yield* HostProcess.Environment).T3CODE_CLI_PATH?.trim(); if (shim && (yield* fs.exists(shim).pipe(Effect.orElseSucceed(() => false)))) { return Option.some(shim); } - return (yield* HostProcessIsExecutable) - ? Option.some(yield* HostProcessExecutablePath) + return (yield* HostProcess.IsExecutable) + ? Option.some(yield* HostProcess.ExecutablePath) : Option.none(); }); @@ -197,7 +191,7 @@ export const resolveRootCliCommand = (subcommand: string) => Effect.gen(function* () { const { command, launcher } = yield* resolveHostCliCommand(subcommand); if (launcher) return `sudo ${command}`; - const executablePath = yield* HostProcessExecutablePath; + const executablePath = yield* HostProcess.ExecutablePath; const systemNode = ROOT_PATH_DIRECTORIES.some((directory) => executablePath.startsWith(`${directory}/`), ); diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index fa7c6b7588fd..56a84bba7177 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -7,7 +7,7 @@ import * as NodePath from "node:path"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { AuthStandardClientScopes } from "@t3tools/contracts"; import * as NetService from "@t3tools/shared/Net"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { assert, describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -184,7 +184,7 @@ describe("t3 pair", () => { }), ).pipe( Effect.provide(NodeServices.layer), - Effect.provideService(HostProcessEnvironment, { + Effect.provideService(HostProcess.Environment, { ...process.env, [SERVICE_LAUNCHER_CONTEXT_ENV]: JSON.stringify({ protocol: SERVICE_LAUNCHER_PROTOCOL, diff --git a/apps/server/src/cli/server.ts b/apps/server/src/cli/server.ts index 3062a840e59e..8c8dd9237cb9 100644 --- a/apps/server/src/cli/server.ts +++ b/apps/server/src/cli/server.ts @@ -1,4 +1,4 @@ -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; @@ -32,7 +32,7 @@ export const runDefaultServerCommand = (flags: CliServerFlags) => if (Option.isSome(flags.cwd)) { const cwd = flags.cwd.value.trim(); const fs = yield* FileSystem.FileSystem; - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const explicitPath = cwd === "." || cwd === ".." || diff --git a/apps/server/src/cli/service.test.ts b/apps/server/src/cli/service.test.ts index f58fe4a17815..077274fc2159 100644 --- a/apps/server/src/cli/service.test.ts +++ b/apps/server/src/cli/service.test.ts @@ -1,6 +1,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, expect, it } from "@effect/vitest"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as NetService from "@t3tools/shared/Net"; import * as ConfigProvider from "effect/ConfigProvider"; import * as Effect from "effect/Effect"; @@ -136,7 +136,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, NetService.layer))("service commands "--base-dir", baseDir, ]).pipe( - Effect.provideService(HostProcessEnvironment, {}), + Effect.provideService(HostProcess.Environment, {}), Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} }))), ); @@ -161,7 +161,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, NetService.layer))("service commands "--base-dir", baseDir, ]).pipe( - Effect.provideService(HostProcessEnvironment, {}), + Effect.provideService(HostProcess.Environment, {}), Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} }))), Effect.flip, ); @@ -190,7 +190,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, NetService.layer))("service commands baseDir, "--allow-downgrade", ]).pipe( - Effect.provideService(HostProcessEnvironment, {}), + Effect.provideService(HostProcess.Environment, {}), Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} }))), ); diff --git a/apps/server/src/cli/service.ts b/apps/server/src/cli/service.ts index 6b799f58e23e..7a5dd7337478 100644 --- a/apps/server/src/cli/service.ts +++ b/apps/server/src/cli/service.ts @@ -1,4 +1,4 @@ -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Console from "effect/Console"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -241,7 +241,7 @@ export const offerServiceDuringOnboarding = Effect.gen(function* () { } // A LaunchAgent starts at login and dies at logout; there is no // enable-linger equivalent on macOS. Do not promise more than that. - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const wanted = yield* Prompt.run( Prompt.Confirm({ message: installed diff --git a/apps/server/src/cli/theme.test.ts b/apps/server/src/cli/theme.test.ts index edf309ccc33b..5dd350043dbe 100644 --- a/apps/server/src/cli/theme.test.ts +++ b/apps/server/src/cli/theme.test.ts @@ -14,11 +14,11 @@ import { Command } from "effect/cli"; import { cli } from "../binCli.ts"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; // These force a failure with chmod, which Windows ignores for directories and // cannot use to make a file unreadable, so the failure never happens there. -const windowsHost = HostProcessPlatform.defaultValue() === "win32"; +const windowsHost = HostProcess.Platform.defaultValue() === "win32"; const runCli = (args: ReadonlyArray) => Command.runWith(cli, { version: "0.0.0" })(args).pipe( diff --git a/apps/server/src/cli/triage.ts b/apps/server/src/cli/triage.ts index a7bd06fab678..7bce1c0251ca 100644 --- a/apps/server/src/cli/triage.ts +++ b/apps/server/src/cli/triage.ts @@ -14,7 +14,7 @@ import * as NodeChildProcess from "node:child_process"; import * as NodeOS from "node:os"; import * as NodeReadlinePromises from "node:readline/promises"; -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { isCommandAvailable, resolveSpawnCommand } from "@t3tools/shared/shell"; import * as Config from "effect/Config"; import * as Console from "effect/Console"; @@ -193,7 +193,7 @@ export const triageCommand = Command.make("triage", { releaseTag: /^[^-+]+-(?:nightly|preview)\./.test(version) ? `v${version} (prerelease build; if this tag does not exist, clone main)` : `v${version}`, - os: `${yield* HostProcessPlatform} ${yield* HostProcessArchitecture} (${NodeOS.release()})`, + os: `${yield* HostProcess.Platform} ${yield* HostProcess.Architecture} (${NodeOS.release()})`, nodeVersion: process.version, launchedAs: yield* resolveCliCommand("triage"), server: yield* describeServerProcess(paths.serverRuntimeStatePath), diff --git a/apps/server/src/cli/uninstall.test.ts b/apps/server/src/cli/uninstall.test.ts index 02860f529440..1e81e8b8c7a9 100644 --- a/apps/server/src/cli/uninstall.test.ts +++ b/apps/server/src/cli/uninstall.test.ts @@ -3,7 +3,7 @@ import { assert, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { findOwnedLauncher } from "./uninstall.ts"; @@ -32,6 +32,6 @@ it.layer(NodeServices.layer)("t3 uninstall launcher", (it) => { assert.isUndefined(yield* findOwnedLauncher({ launchedAs: theirs, versionsDir })); assert.isUndefined(yield* findOwnedLauncher({ launchedAs: copy, versionsDir })); assert.isUndefined(yield* findOwnedLauncher({ launchedAs: undefined, versionsDir })); - }).pipe(Effect.scoped, Effect.provideService(HostProcessPlatform, "linux")), + }).pipe(Effect.scoped, Effect.provideService(HostProcess.Platform, "linux")), ); }); diff --git a/apps/server/src/cli/uninstall.ts b/apps/server/src/cli/uninstall.ts index 4b977bc24951..deec288ef6e8 100644 --- a/apps/server/src/cli/uninstall.ts +++ b/apps/server/src/cli/uninstall.ts @@ -4,11 +4,7 @@ // cannot express: it kills the child when the scope closes. import * as NodeChildProcess from "node:child_process"; -import { - HostProcessEnvironment, - HostProcessIsExecutable, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Console from "effect/Console"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -58,7 +54,7 @@ export const findOwnedLauncher = Effect.fn("cli.uninstall.find_launcher")(functi }) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; if (input.launchedAs === undefined) return undefined; if (platform === "win32") { const shimPath = yield* findWindowsShim(input.launchedAs); @@ -87,7 +83,7 @@ const planUninstall = Effect.fn("cli.uninstall.plan")(function* (input: { path.resolve(status.installedBaseDir) === path.resolve(input.baseDir); const versionsDir = pinnedRuntimeVersionsDir(path, input.baseDir); const runtimeDir = path.dirname(versionsDir); - const launchedAs = (yield* HostProcessIsExecutable) ? yield* resolveLauncherPath : undefined; + const launchedAs = (yield* HostProcess.IsExecutable) ? yield* resolveLauncherPath : undefined; const plan: UninstallPlan = { service: status.supported && status.installed && servesThisHome, launcher: yield* findOwnedLauncher({ launchedAs, versionsDir }), @@ -128,14 +124,14 @@ const runUninstall = Effect.fn("cli.uninstall.run")(function* (input: { readonly assumeYes: boolean; }) { const fs = yield* FileSystem.FileSystem; - const platform = yield* HostProcessPlatform; - const environment = yield* HostProcessEnvironment; + const platform = yield* HostProcess.Platform; + const environment = yield* HostProcess.Environment; const service = yield* BootService.BootService; const plan = yield* planUninstall({ baseDir: input.baseDir }); if (!plan.service && plan.launcher === undefined && plan.runtimeDir === undefined) { yield* Console.log(`Nothing to remove: t3 is not installed for ${input.baseDir}.`); - if (!(yield* HostProcessIsExecutable)) { + if (!(yield* HostProcess.IsExecutable)) { yield* Console.log( " This t3 runs from a Node script, so it was installed by npm or built from source. Remove it the same way (`npm uninstall -g t3`, or delete the checkout).", ); diff --git a/apps/server/src/cli/update.test.ts b/apps/server/src/cli/update.test.ts index 1a9c66428da9..8664e4ba5b45 100644 --- a/apps/server/src/cli/update.test.ts +++ b/apps/server/src/cli/update.test.ts @@ -4,12 +4,7 @@ import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; -import { - HostProcessEnvironment, - HostProcessInvokedAs, - HostProcessPlatform, - HostProcessWorkingDirectory, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { repointLauncher, resolveLauncherPath } from "./update.ts"; @@ -37,7 +32,7 @@ it.layer(NodeServices.layer)("t3 update launcher", (it) => { assert.deepStrictEqual(Option.getOrUndefined(repointed), launcher); assert.equal(yield* fs.readLink(launcher), newExe); - }).pipe(Effect.scoped, Effect.provideService(HostProcessPlatform, "linux")), + }).pipe(Effect.scoped, Effect.provideService(HostProcess.Platform, "linux")), ); it.effect("leaves a plain copy or a foreign symlink alone", () => @@ -71,7 +66,7 @@ it.layer(NodeServices.layer)("t3 update launcher", (it) => { } assert.equal(yield* fs.readLink(foreign), elsewhere); assert.equal(yield* fs.readLink(otherLauncher), otherHome); - }).pipe(Effect.scoped, Effect.provideService(HostProcessPlatform, "linux")), + }).pipe(Effect.scoped, Effect.provideService(HostProcess.Platform, "linux")), ); it.effect("finds the launcher a bare command name resolved to on PATH", () => @@ -84,26 +79,26 @@ it.layer(NodeServices.layer)("t3 update launcher", (it) => { yield* fs.writeFileString(launcher, ""); const bare = yield* resolveLauncherPath.pipe( - Effect.provideService(HostProcessInvokedAs, "t3"), - Effect.provideService(HostProcessEnvironment, { + Effect.provideService(HostProcess.InvokedAs, "t3"), + Effect.provideService(HostProcess.Environment, { PATH: `${path.join(root, "missing")}:${path.join(root, "bin")}`, }), - Effect.provideService(HostProcessWorkingDirectory, root), + Effect.provideService(HostProcess.WorkingDirectory, root), ); const relative = yield* resolveLauncherPath.pipe( - Effect.provideService(HostProcessInvokedAs, "./bin/t3"), - Effect.provideService(HostProcessEnvironment, { PATH: "" }), - Effect.provideService(HostProcessWorkingDirectory, root), + Effect.provideService(HostProcess.InvokedAs, "./bin/t3"), + Effect.provideService(HostProcess.Environment, { PATH: "" }), + Effect.provideService(HostProcess.WorkingDirectory, root), ); const absent = yield* resolveLauncherPath.pipe( - Effect.provideService(HostProcessInvokedAs, "t3"), - Effect.provideService(HostProcessEnvironment, { PATH: path.join(root, "missing") }), - Effect.provideService(HostProcessWorkingDirectory, root), + Effect.provideService(HostProcess.InvokedAs, "t3"), + Effect.provideService(HostProcess.Environment, { PATH: path.join(root, "missing") }), + Effect.provideService(HostProcess.WorkingDirectory, root), ); assert.equal(bare, launcher); assert.equal(relative, launcher); assert.equal(absent, undefined); - }).pipe(Effect.scoped, Effect.provideService(HostProcessPlatform, "linux")), + }).pipe(Effect.scoped, Effect.provideService(HostProcess.Platform, "linux")), ); }); diff --git a/apps/server/src/cli/update.ts b/apps/server/src/cli/update.ts index 576158e316bc..073217d505cb 100644 --- a/apps/server/src/cli/update.ts +++ b/apps/server/src/cli/update.ts @@ -1,11 +1,4 @@ -import { - HostProcessArchitecture, - HostProcessEnvironment, - HostProcessInvokedAs, - HostProcessIsExecutable, - HostProcessPlatform, - HostProcessWorkingDirectory, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { CLI_RELEASE_BASE_URL_ENV, CLI_RELEASE_CHANNELS, @@ -121,7 +114,7 @@ export const repointLauncher = Effect.fn("cli.update.repoint_launcher")(function }) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; if (input.launchedAs === undefined) return Option.none(); const ownsTarget = (candidate: string) => launcherOwnsVersionsDir(path, input.versionsDir, candidate); @@ -170,10 +163,10 @@ export const repointLauncher = Effect.fn("cli.update.repoint_launcher")(function export const resolveLauncherPath = Effect.gen(function* () { const path = yield* Path.Path; const fs = yield* FileSystem.FileSystem; - const invokedAs = yield* HostProcessInvokedAs; - const cwd = yield* HostProcessWorkingDirectory; - const environment = yield* HostProcessEnvironment; - const platform = yield* HostProcessPlatform; + const invokedAs = yield* HostProcess.InvokedAs; + const cwd = yield* HostProcess.WorkingDirectory; + const environment = yield* HostProcess.Environment; + const platform = yield* HostProcess.Platform; if (invokedAs.includes("/") || invokedAs.includes("\\")) { return path.resolve(cwd, invokedAs); } @@ -198,7 +191,7 @@ export const findWindowsShim = Effect.fn("cli.update.find_windows_shim")(functio ) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const environment = yield* HostProcessEnvironment; + const environment = yield* HostProcess.Environment; const candidates = [ ...(environment["T3CODE_INSTALL_BIN_DIR"] ? [environment["T3CODE_INSTALL_BIN_DIR"]] : []), ...(environment["PATH"] ?? environment["Path"] ?? "").split(";"), @@ -299,7 +292,7 @@ const findForegroundServer = Effect.fn("cli.update.find_foreground_server")(func const belongsToBootService = Effect.fn("cli.update.belongs_to_boot_service")(function* ( pid: number, ) { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const fs = yield* FileSystem.FileSystem; const runner = yield* ProcessRunner.ProcessRunner; if (platform === "linux") { @@ -340,9 +333,9 @@ const runUpdate = Effect.fn("cli.update.run")(function* (input: { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const runner = yield* ProcessRunner.ProcessRunner; - const platform = yield* HostProcessPlatform; - const arch = yield* HostProcessArchitecture; - const environment = yield* HostProcessEnvironment; + const platform = yield* HostProcess.Platform; + const arch = yield* HostProcess.Architecture; + const environment = yield* HostProcess.Environment; const httpClient = yield* HttpClient.HttpClient; const service = yield* BootService.BootService; @@ -531,7 +524,7 @@ const runUpdate = Effect.fn("cli.update.run")(function* (input: { ), ); - const launchedAs = (yield* HostProcessIsExecutable) ? yield* resolveLauncherPath : undefined; + const launchedAs = (yield* HostProcess.IsExecutable) ? yield* resolveLauncherPath : undefined; const repointed = yield* repointLauncher({ launchedAs, versionsDir: path.dirname(runtime.versionDir), diff --git a/apps/server/src/cloud/bootService.test.ts b/apps/server/src/cloud/bootService.test.ts index 85de4a7bd437..65bf0818a970 100644 --- a/apps/server/src/cloud/bootService.test.ts +++ b/apps/server/src/cloud/bootService.test.ts @@ -1,10 +1,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { expect, it } from "@effect/vitest"; -import { - HostProcessExecutablePath, - HostProcessPlatform, - HostProcessUserId, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as ConfigProvider from "effect/ConfigProvider"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; @@ -226,9 +222,9 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( Effect.provideService(ProcessRunner.ProcessRunner, runner), Effect.provide( Layer.mergeAll( - Layer.succeed(HostProcessPlatform, platform), - Layer.succeed(HostProcessUserId, 501), - Layer.succeed(HostProcessExecutablePath, "/usr/bin/t3"), + Layer.succeed(HostProcess.Platform, platform), + Layer.succeed(HostProcess.UserId, 501), + Layer.succeed(HostProcess.ExecutablePath, "/usr/bin/t3"), Layer.succeed( HttpClient.HttpClient, HttpClient.make(() => Effect.die("no release download expected")), diff --git a/apps/server/src/cloud/bootService.ts b/apps/server/src/cloud/bootService.ts index 0b102579887a..294f229affa9 100644 --- a/apps/server/src/cloud/bootService.ts +++ b/apps/server/src/cloud/bootService.ts @@ -1,9 +1,4 @@ -import { - HostProcessArchitecture, - HostProcessExecutablePath, - HostProcessPlatform, - HostProcessUserId, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Config from "effect/Config"; import * as Context from "effect/Context"; import * as DateTime from "effect/DateTime"; @@ -558,10 +553,10 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { readonly cliVersion: string; readonly host?: BootServiceHost; }) { - const hostExecPath = yield* HostProcessExecutablePath; - const platform = yield* HostProcessPlatform; - const arch = yield* HostProcessArchitecture; - const uid = yield* HostProcessUserId; + const hostExecPath = yield* HostProcess.ExecutablePath; + const platform = yield* HostProcess.Platform; + const arch = yield* HostProcess.Architecture; + const uid = yield* HostProcess.UserId; const httpClient = yield* HttpClient.HttpClient; const releaseBaseUrl = Option.getOrUndefined( yield* Config.String(CLI_RELEASE_BASE_URL_ENV).pipe(Config.option), diff --git a/apps/server/src/cloud/selfUpdate.test.ts b/apps/server/src/cloud/selfUpdate.test.ts index 111b62c8a481..443fad607fb8 100644 --- a/apps/server/src/cloud/selfUpdate.test.ts +++ b/apps/server/src/cloud/selfUpdate.test.ts @@ -1,7 +1,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { expect, it } from "@effect/vitest"; import { ServerSelfUpdateError, ThreadId } from "@t3tools/contracts"; -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Cause from "effect/Cause"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; @@ -120,8 +120,8 @@ const makeHarness = Effect.fn("test.make_self_update_harness")(function* ( }, ), Effect.provideService(HttpClient.HttpClient, releaseHttpClient(order)), - Effect.provideService(HostProcessPlatform, "linux"), - Effect.provideService(HostProcessArchitecture, "x64"), + Effect.provideService(HostProcess.Platform, "linux"), + Effect.provideService(HostProcess.Architecture, "x64"), Effect.provide(ServerConfig.layer({ ...config, mode: options.mode ?? "web" })), ); return { selfUpdate, order }; diff --git a/apps/server/src/cloud/selfUpdate.ts b/apps/server/src/cloud/selfUpdate.ts index 47c4949de739..615351c4395e 100644 --- a/apps/server/src/cloud/selfUpdate.ts +++ b/apps/server/src/cloud/selfUpdate.ts @@ -6,7 +6,7 @@ import { type ServerSelfUpdateResult, type ThreadId, } from "@t3tools/contracts"; -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Cause from "effect/Cause"; import * as Config from "effect/Config"; import * as Context from "effect/Context"; @@ -176,8 +176,8 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* () { const runner = yield* ProcessRunner.ProcessRunner; const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const platform = yield* HostProcessPlatform; - const arch = yield* HostProcessArchitecture; + const platform = yield* HostProcess.Platform; + const arch = yield* HostProcess.Architecture; // Archive-distributed targets download from GitHub Releases. The client is // optional so callers without one (tests, npm-only hosts) still construct. const httpClient = yield* HttpClient.HttpClient; diff --git a/apps/server/src/cloud/serviceLauncherClient.test.ts b/apps/server/src/cloud/serviceLauncherClient.test.ts index eab0935419a0..dee00b0b203c 100644 --- a/apps/server/src/cloud/serviceLauncherClient.test.ts +++ b/apps/server/src/cloud/serviceLauncherClient.test.ts @@ -1,5 +1,5 @@ import { expect, it } from "@effect/vitest"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; @@ -45,7 +45,7 @@ class FakeLauncherProcess { const makeClient = (host: FakeLauncherProcess, currentVersion: string) => ServiceLauncherClient.make({ currentVersion }).pipe( Effect.provideService(ServiceLauncherClient.ServiceLauncherHostProcess, host), - Effect.provideService(HostProcessEnvironment, host.env), + Effect.provideService(HostProcess.Environment, host.env), ); it.effect("waits for the launcher to durably commit the trial update ID", () => diff --git a/apps/server/src/cloud/serviceLauncherClient.ts b/apps/server/src/cloud/serviceLauncherClient.ts index 2d644157211f..b58d133494cb 100644 --- a/apps/server/src/cloud/serviceLauncherClient.ts +++ b/apps/server/src/cloud/serviceLauncherClient.ts @@ -1,5 +1,5 @@ import type { ServerSelfUpdateOutcome } from "@t3tools/contracts"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -114,7 +114,7 @@ export class ServiceLauncherClient extends Context.Service< const resolveStartup = Effect.fn("cloud.service_launcher_client.resolve_startup")( function* (options?: { readonly currentVersion?: string }) { const host = yield* ServiceLauncherHostProcess; - const environment = yield* HostProcessEnvironment; + const environment = yield* HostProcess.Environment; const currentVersion = options?.currentVersion ?? packageJson.version; const rawContext = environment[SERVICE_LAUNCHER_CONTEXT_ENV]; const context = rawContext === undefined ? undefined : decodeServiceLauncherContext(rawContext); diff --git a/apps/server/src/device/AgentDeviceShim.ts b/apps/server/src/device/AgentDeviceShim.ts index a5a669d1d8ba..f3a21fe9b362 100644 --- a/apps/server/src/device/AgentDeviceShim.ts +++ b/apps/server/src/device/AgentDeviceShim.ts @@ -5,7 +5,7 @@ * types `agent-device …` and gets the version the injected instructions were * written for, regardless of what is or is not globally installed. */ -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveNodeExecutable } from "@t3tools/shared/nodeRuntime"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -20,7 +20,7 @@ export const ensureAgentDeviceShim = Effect.fn("AgentDeviceShim.ensure")(functio const { entryPath } = input; const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const node = yield* resolveNodeExecutable("Device automation"); const shimDir = path.join(input.stateDir, SHIM_DIR); yield* fs.makeDirectory(shimDir, { recursive: true }); diff --git a/apps/server/src/device/AgentDeviceTarget.test.ts b/apps/server/src/device/AgentDeviceTarget.test.ts index 4dfd5e20d1c8..2cedbe1e8bed 100644 --- a/apps/server/src/device/AgentDeviceTarget.test.ts +++ b/apps/server/src/device/AgentDeviceTarget.test.ts @@ -1,9 +1,5 @@ // @effect-diagnostics nodeBuiltinImport:off - exercises concurrent real CLI subprocesses. -import { - HostProcessExecutablePath, - HostProcessIsExecutable, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { describe, expect, it } from "@effect/vitest"; import * as NodeChildProcess from "node:child_process"; import * as NodeUtil from "node:util"; @@ -26,7 +22,7 @@ describe("host-bound agent commands", () => { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const temp = yield* fs.makeTempDirectoryScoped({ prefix: "t3-device-target-" }); - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const dir = path.join( temp, platform === "win32" ? "paths with spaces" : "quotes '\" $HOME `literal`", @@ -92,8 +88,8 @@ if (process.env.AGENT_DEVICE_DAEMON_BASE_URL) process.exit(2);`, } }).pipe( Effect.scoped, - Effect.provideService(HostProcessIsExecutable, true), - Effect.provideService(HostProcessExecutablePath, "/packaged/t3"), + Effect.provideService(HostProcess.IsExecutable, true), + Effect.provideService(HostProcess.ExecutablePath, "/packaged/t3"), Effect.provide(NodeServices.layer), ), ); diff --git a/apps/server/src/device/LocalDeviceHost.test.ts b/apps/server/src/device/LocalDeviceHost.test.ts index 7dde6263defd..9b404099e1dd 100644 --- a/apps/server/src/device/LocalDeviceHost.test.ts +++ b/apps/server/src/device/LocalDeviceHost.test.ts @@ -1,11 +1,6 @@ import { describe, expect, it } from "@effect/vitest"; import * as NodePath from "@effect/platform-node/NodePath"; -import { - HostProcessEnvironment, - HostProcessPlatform, - HostProcessIsExecutable, - HostProcessUserId, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; @@ -26,8 +21,8 @@ const diagnose = ( platform: NodeJS.Platform = "darwin", ) => LocalDeviceHost.__testing.platformReason("android").pipe( - Effect.provideService(HostProcessEnvironment, environment), - Effect.provideService(HostProcessPlatform, platform), + Effect.provideService(HostProcess.Environment, environment), + Effect.provideService(HostProcess.Platform, platform), Effect.provideService( FileSystem.FileSystem, FileSystem.makeNoop({ @@ -155,8 +150,8 @@ it.effect( ] as const) { const environment = { PATH: "/usr/bin", XDG_RUNTIME_DIR: runtimeDir }; const result = yield* LocalDeviceHost.__testing.hubEnvironment(environment).pipe( - Effect.provideService(HostProcessPlatform, platform), - Effect.provideService(HostProcessUserId, uid), + Effect.provideService(HostProcess.Platform, platform), + Effect.provideService(HostProcess.UserId, uid), Effect.provideService( FileSystem.FileSystem, FileSystem.makeNoop({ @@ -176,8 +171,8 @@ it.effect( const missing = yield* LocalDeviceHost.__testing .hubEnvironment({}) .pipe( - Effect.provideService(HostProcessPlatform, "linux"), - Effect.provideService(HostProcessUserId, 1000), + Effect.provideService(HostProcess.Platform, "linux"), + Effect.provideService(HostProcess.UserId, 1000), Effect.provideService(FileSystem.FileSystem, FileSystem.makeNoop({})), ); expect(missing.XDG_RUNTIME_DIR).toBeUndefined(); @@ -192,8 +187,8 @@ it.effect( const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-device-consent-" }); const host = yield* LocalDeviceHost.make().pipe( Effect.provide(Layer.mergeAll(ServerConfig.layerTest(baseDir, baseDir), NetService.layer)), - Effect.provideService(HostProcessEnvironment, { HOME: baseDir, PATH: "" }), - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Environment, { HOME: baseDir, PATH: "" }), + Effect.provideService(HostProcess.Platform, "linux"), Effect.provideService( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(() => @@ -213,7 +208,7 @@ it.effect( expect(yield* host.current).toBeNull(); const error = yield* host .ensureReady(() => Effect.die("Must not install without Node")) - .pipe(Effect.flip, Effect.provideService(HostProcessIsExecutable, true)); + .pipe(Effect.flip, Effect.provideService(HostProcess.IsExecutable, true)); expect(error.message).toContain("Local device support requires Node.js"); expect(error.message).toContain("Install Node.js"); yield* host.stop; diff --git a/apps/server/src/device/LocalDeviceHost.ts b/apps/server/src/device/LocalDeviceHost.ts index 58179151877c..779221ab93cf 100644 --- a/apps/server/src/device/LocalDeviceHost.ts +++ b/apps/server/src/device/LocalDeviceHost.ts @@ -19,11 +19,7 @@ import { LOCAL_DEVICE_HOST_ID, } from "@t3tools/contracts"; import { waitForHttpReady } from "@t3tools/shared/httpReadiness"; -import { - HostProcessEnvironment, - HostProcessPlatform, - HostProcessUserId, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveNodeExecutable, type NodeRuntimeUnavailableError, @@ -107,7 +103,7 @@ interface RunningHost { const platformReason = Effect.fn("LocalDeviceHost.platformReason")(function* ( platform: DevicePlatform, ): Effect.fn.Return { - const hostPlatform = yield* HostProcessPlatform; + const hostPlatform = yield* HostProcess.Platform; if (platform === "ios") { if (hostPlatform !== "darwin") return "iOS Simulators need macOS with Xcode."; if (!(yield* isCommandAvailable("xcrun"))) return "Xcode command line tools were not found."; @@ -132,8 +128,8 @@ const platformReason = Effect.fn("LocalDeviceHost.platformReason")(function* ( const androidSdk = Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const environment = yield* HostProcessEnvironment; - const platform = yield* HostProcessPlatform; + const environment = yield* HostProcess.Environment; + const platform = yield* HostProcess.Platform; const home = environment.HOME ?? environment.USERPROFILE ?? ""; const explicit = environment.ANDROID_HOME?.trim() || environment.ANDROID_SDK_ROOT?.trim(); const candidates = explicit @@ -208,8 +204,8 @@ const hubEnvironment = Effect.fn("LocalDeviceHost.hubEnvironment")(function* ( environment: NodeJS.ProcessEnv, ) { const env: NodeJS.ProcessEnv = { ...environment, FORCE_COLOR: "0", NO_COLOR: "1" }; - const platform = yield* HostProcessPlatform; - const uid = yield* HostProcessUserId; + const platform = yield* HostProcess.Platform; + const uid = yield* HostProcess.UserId; if (platform === "linux" && env.XDG_RUNTIME_DIR === undefined && uid !== undefined) { // SSH sessions may omit the directory where the emulator publishes its gRPC token. const runtimeDir = `/run/user/${uid}`; @@ -234,8 +230,8 @@ export const make = Effect.fn("LocalDeviceHost.make")(function* () { const net = yield* NetService.NetService; const runner = yield* ProcessRunner.ProcessRunner; const httpClient = yield* HttpClient.HttpClient; - const environment = yield* HostProcessEnvironment; - const hostPlatform = yield* HostProcessPlatform; + const environment = yield* HostProcess.Environment; + const hostPlatform = yield* HostProcess.Platform; const sdk = yield* androidSdk; const hostEnvironment = deviceHostEnvironment(environment, sdk.root, hostPlatform, path); const startLock = yield* Semaphore.make(1); @@ -406,7 +402,7 @@ export const make = Effect.fn("LocalDeviceHost.make")(function* () { stderr: "pipe", env: yield* hubEnvironment(hostEnvironment).pipe( Effect.provideService(FileSystem.FileSystem, fs), - Effect.provideService(HostProcessPlatform, hostPlatform), + Effect.provideService(HostProcess.Platform, hostPlatform), ), }, ), @@ -598,7 +594,7 @@ export const make = Effect.fn("LocalDeviceHost.make")(function* () { const nodePath = yield* resolveNodeExecutable("Local device support", hostEnvironment).pipe( Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path), - Effect.provideService(HostProcessPlatform, hostPlatform), + Effect.provideService(HostProcess.Platform, hostPlatform), ); const installed = yield* isDeviceHubInstalled(config.baseDir).pipe( Effect.provideService(FileSystem.FileSystem, fs), diff --git a/apps/server/src/device/sshDeviceScript.test.ts b/apps/server/src/device/sshDeviceScript.test.ts index f1f56c715e60..97f124721bb0 100644 --- a/apps/server/src/device/sshDeviceScript.test.ts +++ b/apps/server/src/device/sshDeviceScript.test.ts @@ -1,6 +1,6 @@ // @effect-diagnostics nodeBuiltinImport:off globalFetchInEffect:off - verifies generated remote scripts using real shell and Node processes. import * as Effect from "effect/Effect"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { describe, expect, it } from "@effect/vitest"; import * as NodeChildProcess from "node:child_process"; import * as NodeFSP from "node:fs/promises"; @@ -14,7 +14,7 @@ const exec = NodeUtil.promisify(NodeChildProcess.execFile); it.effect("finds Android Studio Java for a non-interactive SSH session", () => Effect.gen(function* () { - if ((yield* HostProcessPlatform) === "win32") return; + if ((yield* HostProcess.Platform) === "win32") return; yield* Effect.promise(async () => { const home = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-ssh-java-")); try { @@ -38,7 +38,7 @@ it.effect("finds Android Studio Java for a non-interactive SSH session", () => it.effect("preserves shell metacharacters and newlines in remote arguments", () => Effect.gen(function* () { - if ((yield* HostProcessPlatform) === "win32") return; + if ((yield* HostProcess.Platform) === "win32") return; yield* Effect.promise(async () => { const value = "quotes ' \" ; $(echo expanded) $HOME\nnext line"; const result = await exec("sh", ["-c", `printf %s ${quoteRemoteArg(value)}`]); @@ -50,7 +50,7 @@ it.effect("preserves shell metacharacters and newlines in remote arguments", () describe("remote helper lifecycle", () => { it.effect("reuses its own healthy helpers and stops only its own runtime", () => Effect.gen(function* () { - if ((yield* HostProcessPlatform) === "win32") return; + if ((yield* HostProcess.Platform) === "win32") return; yield* Effect.promise(async () => { const home = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-remote-script-")); const bin = NodePath.join(home, "bin"); diff --git a/apps/server/src/environment/RemoteOpenTargets.test.ts b/apps/server/src/environment/RemoteOpenTargets.test.ts index 1e222abb867e..8a90a9345df3 100644 --- a/apps/server/src/environment/RemoteOpenTargets.test.ts +++ b/apps/server/src/environment/RemoteOpenTargets.test.ts @@ -1,5 +1,5 @@ import { it } from "@effect/vitest"; -import { HostProcessHostname } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as NetService from "@t3tools/shared/Net"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -54,7 +54,7 @@ const resolveTargets = (input: { readonly hostname: string; }) => Effect.flatMap(RemoteOpenTargets.RemoteOpenTargets, (service) => service.resolveTargets()).pipe( - Effect.provideService(HostProcessHostname, input.hostname), + Effect.provideService(HostProcess.Hostname, input.hostname), Effect.provide( RemoteOpenTargets.layer.pipe( Layer.provide(Layer.mergeAll(layerNet(input.sshd), layerSpawner(input.tailscale))), diff --git a/apps/server/src/environment/RemoteOpenTargets.ts b/apps/server/src/environment/RemoteOpenTargets.ts index e0acfaee2f48..1cd5751bc2d4 100644 --- a/apps/server/src/environment/RemoteOpenTargets.ts +++ b/apps/server/src/environment/RemoteOpenTargets.ts @@ -9,7 +9,7 @@ * on the tailnet; `.local` only on the same LAN). */ import { type RemoteOpenTarget } from "@t3tools/contracts"; -import { HostProcessHostname } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as NetService from "@t3tools/shared/Net"; import { readTailscaleStatus } from "@t3tools/tailscale"; import * as Context from "effect/Context"; @@ -58,7 +58,7 @@ export const make = Effect.gen(function* () { // os.hostname() may already be an FQDN (macOS often reports // "Name.local"); mDNS names are always `.local`. - const hostname = yield* HostProcessHostname; + const hostname = yield* HostProcess.Hostname; const shortHostname = hostname.split(".")[0]?.trim(); if (shortHostname !== undefined && shortHostname.length > 0) { targets.push({ kind: "mdns", host: `${shortHostname}.local` }); diff --git a/apps/server/src/environment/ServerEnvironment.test.ts b/apps/server/src/environment/ServerEnvironment.test.ts index 41c18b907437..f9e13212252f 100644 --- a/apps/server/src/environment/ServerEnvironment.test.ts +++ b/apps/server/src/environment/ServerEnvironment.test.ts @@ -10,12 +10,7 @@ import * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; import { DEFAULT_SIGNAL_EXPORT } from "@t3tools/shared/observability"; -import { - HostProcessArguments, - HostProcessEnvironment, - HostProcessIsExecutable, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as OtelEnvironment from "@t3tools/shared/otelEnvironment"; import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; @@ -112,10 +107,10 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => { Layer.provide(ServerConfig.layer({ ...config, mode })), ), ), - Effect.provideService(HostProcessArguments, ["node", entry]), - Effect.provideService(HostProcessIsExecutable, false), - Effect.provideService(HostProcessPlatform, "linux"), - Effect.provideService(HostProcessEnvironment, {}), + Effect.provideService(HostProcess.Arguments, ["node", entry]), + Effect.provideService(HostProcess.IsExecutable, false), + Effect.provideService(HostProcess.Platform, "linux"), + Effect.provideService(HostProcess.Environment, {}), ); expect(descriptor.capabilities.serverInstallation).toEqual( mode === "web" ? { kind: "npm-global", prefix } : undefined, diff --git a/apps/server/src/environment/ServerEnvironment.ts b/apps/server/src/environment/ServerEnvironment.ts index 6381547468ae..7fc4a66464e9 100644 --- a/apps/server/src/environment/ServerEnvironment.ts +++ b/apps/server/src/environment/ServerEnvironment.ts @@ -4,7 +4,7 @@ import { PROVIDER_SEND_TURN_MAX_FILE_BYTES, type ExecutionEnvironmentDescriptor, } from "@t3tools/contracts"; -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; @@ -186,8 +186,8 @@ export const make = Effect.gen(function* () { const serverConfig = yield* ServerConfig.ServerConfig; const secrets = yield* ServerSecretStore.ServerSecretStore; const identity = yield* ServerEnvironmentIdentity; - const hostPlatform = yield* HostProcessPlatform; - const hostArchitecture = yield* HostProcessArchitecture; + const hostPlatform = yield* HostProcess.Platform; + const hostArchitecture = yield* HostProcess.Architecture; const environmentId = yield* identity.getEnvironmentId; const cwdBaseName = path.basename(serverConfig.cwd).trim(); const label = yield* resolveServerEnvironmentLabel({ cwdBaseName }); diff --git a/apps/server/src/environment/ServerEnvironmentLabel.test.ts b/apps/server/src/environment/ServerEnvironmentLabel.test.ts index fd7f8ccb3726..7fd418e2821e 100644 --- a/apps/server/src/environment/ServerEnvironmentLabel.test.ts +++ b/apps/server/src/environment/ServerEnvironmentLabel.test.ts @@ -7,7 +7,7 @@ import * as PlatformError from "effect/PlatformError"; import * as References from "effect/References"; import * as Schema from "effect/Schema"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; -import { HostProcessHostname, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { vi } from "vite-plus/test"; import * as ProcessRunner from "../processRunner.ts"; @@ -52,8 +52,8 @@ const withHostPlatform = ( ) => Layer.mergeAll( layer, - Layer.succeed(HostProcessPlatform, platform), - Layer.succeed(HostProcessHostname, hostname), + Layer.succeed(HostProcess.Platform, platform), + Layer.succeed(HostProcess.Hostname, hostname), ); afterEach(() => { diff --git a/apps/server/src/environment/ServerEnvironmentLabel.ts b/apps/server/src/environment/ServerEnvironmentLabel.ts index 4a5d0a009b9a..73ff4a763910 100644 --- a/apps/server/src/environment/ServerEnvironmentLabel.ts +++ b/apps/server/src/environment/ServerEnvironmentLabel.ts @@ -1,4 +1,4 @@ -import { HostProcessHostname, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; @@ -151,7 +151,7 @@ const runFriendlyLabelCommand = Effect.fn("runFriendlyLabelCommand")(function* ( }); const resolveFriendlyHostLabel = Effect.fn("resolveFriendlyHostLabel")(function* () { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; if (platform === "darwin") { return yield* runFriendlyLabelCommand({ probe: "macos-computer-name", @@ -187,7 +187,7 @@ export const resolveServerEnvironmentLabel = Effect.fn("resolveServerEnvironment return friendlyHostLabel; } - const hostname = normalizeLabel(yield* HostProcessHostname); + const hostname = normalizeLabel(yield* HostProcess.Hostname); if (hostname) { return hostname; } diff --git a/apps/server/src/environment/ServerEnvironmentMachine.test.ts b/apps/server/src/environment/ServerEnvironmentMachine.test.ts index 3c2ab52a7083..198aa4ed5583 100644 --- a/apps/server/src/environment/ServerEnvironmentMachine.test.ts +++ b/apps/server/src/environment/ServerEnvironmentMachine.test.ts @@ -4,7 +4,7 @@ import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as PlatformError from "effect/PlatformError"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { vi } from "vite-plus/test"; import * as ProcessRunner from "../processRunner.ts"; @@ -52,7 +52,7 @@ const layerDmiFileSystem = (files: Readonly>) => }); const withPlatform = (platform: NodeJS.Platform, fileSystem = FileSystem.layerNoop({})) => - Layer.mergeAll(layerProcessRunnerTest, fileSystem, Layer.succeed(HostProcessPlatform, platform)); + Layer.mergeAll(layerProcessRunnerTest, fileSystem, Layer.succeed(HostProcess.Platform, platform)); afterEach(() => { runMock.mockReset(); diff --git a/apps/server/src/environment/ServerEnvironmentMachine.ts b/apps/server/src/environment/ServerEnvironmentMachine.ts index e9f5e51af600..267dd42a532c 100644 --- a/apps/server/src/environment/ServerEnvironmentMachine.ts +++ b/apps/server/src/environment/ServerEnvironmentMachine.ts @@ -1,5 +1,5 @@ import type { EnvironmentMachineKind } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -161,7 +161,7 @@ const detectLinuxMachineKind = Effect.fn("detectLinuxMachineKind")(function* () export const detectServerEnvironmentMachineKind = Effect.fn("detectServerEnvironmentMachineKind")( function* () { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; switch (platform) { case "darwin": return yield* detectDarwinMachineKind(); diff --git a/apps/server/src/htmlRender/HtmlRender.test.ts b/apps/server/src/htmlRender/HtmlRender.test.ts index a6a3e56564dc..930fe11c0a8c 100644 --- a/apps/server/src/htmlRender/HtmlRender.test.ts +++ b/apps/server/src/htmlRender/HtmlRender.test.ts @@ -2,7 +2,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { describe, expect, it } from "@effect/vitest"; import { ThreadId } from "@t3tools/contracts"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { HTML_RENDER_MEASURE_FONTS, HTML_RENDER_MEASURE_WIDTHS, @@ -238,7 +238,7 @@ describe("HtmlRender", () => { "screenshots the page in headless Chrome with the requested theme and every console level", (ctx) => Effect.gen(function* () { - const executable = (yield* HostProcessEnvironment)[TEST_BROWSER_ENV]; + const executable = (yield* HostProcess.Environment)[TEST_BROWSER_ENV]; if (!executable) return ctx.skip(`Set ${TEST_BROWSER_ENV} to run this test.`); yield* Effect.gen(function* () { const htmlRender = yield* HtmlRender.HtmlRender; @@ -292,7 +292,7 @@ describe("HtmlRender", () => { "keeps every local file but the page itself out of the browser", (ctx) => Effect.gen(function* () { - const executable = (yield* HostProcessEnvironment)[TEST_BROWSER_ENV]; + const executable = (yield* HostProcess.Environment)[TEST_BROWSER_ENV]; if (!executable) return ctx.skip(`Set ${TEST_BROWSER_ENV} to run this test.`); yield* Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; @@ -323,7 +323,7 @@ describe("HtmlRender", () => { "keeps the page off this machine's local network", (ctx) => Effect.gen(function* () { - const executable = (yield* HostProcessEnvironment)[TEST_BROWSER_ENV]; + const executable = (yield* HostProcess.Environment)[TEST_BROWSER_ENV]; if (!executable) return ctx.skip(`Set ${TEST_BROWSER_ENV} to run this test.`); // The page's connections go through a proxy that only reaches public // addresses, and WebRTC is gone. Each line below is a way out that @@ -381,7 +381,7 @@ describe("HtmlRender", () => { "measures a published page at every client width with a fresh load each", (ctx) => Effect.gen(function* () { - const executable = (yield* HostProcessEnvironment)[TEST_BROWSER_ENV]; + const executable = (yield* HostProcess.Environment)[TEST_BROWSER_ENV]; if (!executable) return ctx.skip(`Set ${TEST_BROWSER_ENV} to run this test.`); yield* Effect.gen(function* () { const htmlRender = yield* HtmlRender.HtmlRender; diff --git a/apps/server/src/htmlRender/HtmlRender.ts b/apps/server/src/htmlRender/HtmlRender.ts index cfbf6c206ffb..47c7238fb476 100644 --- a/apps/server/src/htmlRender/HtmlRender.ts +++ b/apps/server/src/htmlRender/HtmlRender.ts @@ -1,5 +1,5 @@ import type { ThreadId } from "@t3tools/contracts"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { clampHtmlRenderHeight, HTML_RENDER_COLUMN_WIDTH, @@ -341,7 +341,7 @@ const make = Effect.gen(function* () { const browsers = yield* Semaphore.make(MAX_CONCURRENT_BROWSERS); // Chrome's sandbox stays on unless the operator explicitly turns it off. // Chrome also refuses it as root, where that opt-out is the only way to run. - const noSandbox = PreviewBrowserHost.sandboxDisabled(yield* HostProcessEnvironment); + const noSandbox = PreviewBrowserHost.sandboxDisabled(yield* HostProcess.Environment); const setupCommand = yield* resolveRootCliCommand(PreviewBrowserHost.SETUP_SUBCOMMAND); /** Runs one browser launch; a host that cannot start it gets setup steps instead. */ diff --git a/apps/server/src/keybindings.test.ts b/apps/server/src/keybindings.test.ts index f6c3cb9e8843..0ea44fb3ca07 100644 --- a/apps/server/src/keybindings.test.ts +++ b/apps/server/src/keybindings.test.ts @@ -12,7 +12,7 @@ import * as Schema from "effect/Schema"; import * as ServerConfig from "./config.ts"; import * as Keybindings from "./keybindings.ts"; import { KeybindingsConfigError, MAX_KEYBINDINGS_COUNT } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig); const encodeKeybindingsConfigJson = Schema.encodeEffect(KeybindingsConfigJson); @@ -564,7 +564,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { ); // chmod cannot make a directory unwritable on Windows, so the write succeeds. - it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")( + it.effect.skipIf(HostProcess.Platform.defaultValue() === "win32")( "fails when config directory is not writable", () => Effect.gen(function* () { diff --git a/apps/server/src/mcp/toolkits/device/handlers.ts b/apps/server/src/mcp/toolkits/device/handlers.ts index 1543bbc87100..669d90f23ac5 100644 --- a/apps/server/src/mcp/toolkits/device/handlers.ts +++ b/apps/server/src/mcp/toolkits/device/handlers.ts @@ -9,7 +9,7 @@ import { } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as Path from "effect/Path"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as ServerConfig from "../../../config.ts"; import { ensureAgentDeviceShim } from "../../../device/AgentDeviceShim.ts"; import { nodeRuntimeUnavailableMessage } from "@t3tools/shared/nodeRuntime"; @@ -179,7 +179,7 @@ const handlers = { const targetArgs = [...agentDeviceTargetArgs(device), ...agentArgs]; const config = yield* ServerConfig.ServerConfig; const path = yield* Path.Path; - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const shimDir = yield* ensureAgentDeviceShim({ entryPath: yield* devices.agentCli, stateDir: config.stateDir, diff --git a/apps/server/src/observability/HeapSnapshot.ts b/apps/server/src/observability/HeapSnapshot.ts index 827dfab2c6d6..6359c1628c58 100644 --- a/apps/server/src/observability/HeapSnapshot.ts +++ b/apps/server/src/observability/HeapSnapshot.ts @@ -2,7 +2,7 @@ import * as NodePath from "node:path"; import * as NodeV8 from "node:v8"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -40,7 +40,7 @@ export const writeHeapSnapshot = Effect.fn("server.heapSnapshot", { root: true } */ export const layer = Layer.effectDiscard( Effect.gen(function* () { - if ((yield* HostProcessPlatform) === "win32") return; + if ((yield* HostProcess.Platform) === "win32") return; const { logsDir } = yield* ServerConfig.ServerConfig; const runFork = Effect.runForkWith(yield* Effect.context()); const onSignal = () => void runFork(writeHeapSnapshot(logsDir)); diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index 0802fef78f9e..8651890a2052 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -27,7 +27,7 @@ import { type OrchestrationV2ProviderThread, } from "@t3tools/contracts"; import { GrokSettings } from "@t3tools/provider-grok/settings"; -import { HostProcessIsExecutable, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; @@ -705,7 +705,7 @@ describe("AcpAdapterV2", () => { const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const path = yield* Path.Path; const selfInvocation = yield* resolveSelfInvocation().pipe( - Effect.provideService(HostProcessIsExecutable, true), + Effect.provideService(HostProcess.IsExecutable, true), ); const mockAgentPath = yield* path.fromFileUrl( new URL("../../../scripts/acp-mock-agent.ts", import.meta.url), @@ -1833,7 +1833,7 @@ describe("AcpAdapterV2", () => { it.live("cleans detached fixtures when an assertion aborts the test scope", () => Effect.gen(function* () { - if ((yield* HostProcessPlatform) !== "linux") return; + if ((yield* HostProcess.Platform) !== "linux") return; const fileSystem = yield* FileSystem.FileSystem; let published: ReadonlyArray = []; const failed = yield* Effect.scoped( @@ -1947,7 +1947,7 @@ describe("AcpAdapterV2", () => { it.live("reaps detached native work when the provider exits before explicit teardown", () => Effect.gen(function* () { - if ((yield* HostProcessPlatform) !== "linux") return; + if ((yield* HostProcess.Platform) !== "linux") return; const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -2026,7 +2026,7 @@ describe("AcpAdapterV2", () => { it.live("surfaces reduced guarantee when delegated cgroup containment is unavailable", () => Effect.gen(function* () { - if ((yield* HostProcessPlatform) !== "linux") return; + if ((yield* HostProcess.Platform) !== "linux") return; const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const path = yield* Path.Path; const selfInvocation = yield* resolveSelfInvocation(); @@ -3100,7 +3100,7 @@ describe("AcpAdapterV2", () => { instanceId, settings: DEFAULT_GROK_SETTINGS, environment: {}, - hostPlatform: yield* HostProcessPlatform, + hostPlatform: yield* HostProcess.Platform, selfInvocation: yield* resolveSelfInvocation(), // Production Grok runtimes are wrapped by the x.ai prompt runtime. makeRuntime: (input) => @@ -3147,7 +3147,7 @@ describe("AcpAdapterV2", () => { instanceId, settings: DEFAULT_GROK_SETTINGS, environment: {}, - hostPlatform: yield* HostProcessPlatform, + hostPlatform: yield* HostProcess.Platform, selfInvocation: yield* resolveSelfInvocation(), // Production Grok runtimes are wrapped by the x.ai prompt runtime. makeRuntime: (input) => @@ -13147,7 +13147,7 @@ describe("AcpAdapterV2", () => { it.live("direct Stop skips uninterruptible ACP cancel and recovers after native teardown", () => Effect.gen(function* () { - if ((yield* HostProcessPlatform) !== "linux") return; + if ((yield* HostProcess.Platform) !== "linux") return; const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const fileSystem = yield* FileSystem.FileSystem; const idAllocator = yield* IdAllocator.IdAllocatorV2; @@ -13567,7 +13567,7 @@ describe("AcpAdapterV2", () => { it.live("restart_active terminates native work and reloads a clean runtime", () => Effect.gen(function* () { - if ((yield* HostProcessPlatform) !== "linux") return; + if ((yield* HostProcess.Platform) !== "linux") return; const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const fileSystem = yield* FileSystem.FileSystem; const idAllocator = yield* IdAllocator.IdAllocatorV2; diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 9c4e336815f7..6777f0a2902a 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -47,7 +47,7 @@ import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; import { Tool } from "effect/ai"; import { formatClaudeResumeCompactionQuestion } from "@t3tools/shared/claudeCompaction"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { SpawnExecutableResolution } from "@t3tools/shared/shell"; import { attachmentRelativePath } from "../../attachmentStore.ts"; @@ -1171,7 +1171,7 @@ describe("ClaudeAdapterV2 executable path", () => { const npmDir = "C:\\Users\\dev\\AppData\\Roaming\\npm"; const packageExe = `${npmDir}\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe`; const executablePaths = yield* captureSdkExecutablePaths("claude").pipe( - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), Effect.provideService(SpawnExecutableResolution, () => `${npmDir}\\claude.cmd`), Effect.provideService(ClaudeExecutableFileCheck, (filePath) => filePath === packageExe), ); diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts index cc8a5bef6f5a..730ed8a575f2 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts @@ -8,7 +8,7 @@ import { type SDKUserMessage, } from "@anthropic-ai/claude-agent-sdk"; import * as NodeServices from "@effect/platform-node/NodeServices"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { SpawnExecutableResolution } from "@t3tools/shared/shell"; import { ProviderReplayEntry, @@ -1440,7 +1440,7 @@ async function recordMessagesUntilToolUse(input: { const resolveClaudeRecordingExecutablePath = Effect.fn("resolveClaudeRecordingExecutablePath")( function* (environment: NodeJS.ProcessEnv) { const resolveExecutable = yield* SpawnExecutableResolution; - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const resolved = resolveExecutable("claude", platform, environment); if (resolved === undefined) { return undefined; diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 33db150ec689..79d7bdd7e115 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -31,7 +31,7 @@ import type { WebSearchOutput, } from "@anthropic-ai/claude-agent-sdk/sdk-tools"; import { parseCliArgs } from "@t3tools/shared/cliArgs"; -import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { applyClaudePromptEffortPrefix } from "@t3tools/shared/model"; import { CLAUDE_RESUME_COMPACTION_NEVER_ANSWER, @@ -8176,7 +8176,7 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")( ) { const { instanceId, environment, enabled, config } = input; const fileSystem = yield* FileSystem.FileSystem; - const hostEnvironment = yield* HostProcessEnvironment; + const hostEnvironment = yield* HostProcess.Environment; const idAllocator = yield* IdAllocator.IdAllocatorV2; const queryRunner = yield* ClaudeAgentSdkQueryRunner; const serverConfig = yield* ServerConfig.ServerConfig; @@ -8186,7 +8186,7 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")( const path = yield* Path.Path; const crypto = yield* Crypto.Crypto; const binaryPath = yield* resolveClaudeSdkExecutablePath( - expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory), + expandHomePath(config.binaryPath, yield* HostProcess.HomeDirectory), claudeEnvironment, ); return yield* makeClaudeAdapterV2({ @@ -8231,7 +8231,7 @@ const makeDefaultClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2.layer")(function* const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const crypto = yield* Crypto.Crypto; - const hostEnvironment = yield* HostProcessEnvironment; + const hostEnvironment = yield* HostProcess.Environment; const idAllocator = yield* IdAllocator.IdAllocatorV2; const queryRunner = yield* ClaudeAgentSdkQueryRunner; const serverConfig = yield* ServerConfig.ServerConfig; diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index 5b18f436c26e..9a1c8f6c900f 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -29,7 +29,7 @@ import { TurnItemId, } from "@t3tools/contracts"; import { assert, describe, it } from "@effect/vitest"; -import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { SpawnExecutableResolution } from "@t3tools/shared/shell"; import * as CodexClient from "effect-codex-app-server/client"; import * as CodexError from "effect-codex-app-server/errors"; @@ -698,8 +698,8 @@ describe("CodexAdapterV2 process spawning", () => { assert.deepEqual(command.options.env, { CUSTOM: "1" }); assert.equal(command.options.extendEnv, true); }).pipe( - Effect.provideService(HostProcessPlatform, "win32"), - Effect.provideService(HostProcessEnvironment, { + Effect.provideService(HostProcess.Platform, "win32"), + Effect.provideService(HostProcess.Environment, { PATH: "C:\\Windows\\System32", HOST_ONLY: "1", }), @@ -726,7 +726,7 @@ describe("CodexAdapterV2 process spawning", () => { assert.deepEqual(command.args, ["app-server"]); assert.equal(command.options.shell, false); }).pipe( - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), Effect.provideService(SpawnExecutableResolution, () => "C:\\bin\\codex.exe"), ), ); @@ -774,7 +774,7 @@ describe("CodexAdapterV2 process spawning", () => { ["app-server", "--strict-config", "-c", "model_reasoning_summary=detailed"], ["app-server", "--enable", "env-feature"], ]); - }).pipe(Effect.provideService(HostProcessPlatform, "linux")), + }).pipe(Effect.provideService(HostProcess.Platform, "linux")), ); it.effect("expands ~ in the configured binary path before spawning", () => @@ -821,7 +821,7 @@ describe("CodexAdapterV2 process spawning", () => { Effect.provide( Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), ), - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), ), ); }); diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 69eb0aa7f38e..87c116250c5b 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -30,7 +30,7 @@ import { type ProviderSetupError, } from "@t3tools/contracts"; import { SKILL_MENTION_PATTERN } from "@t3tools/shared/composerInlineTokens"; -import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { dynamicToolTitle } from "@t3tools/shared/toolActivity"; import { getModelSelectionStringOptionValue, modelSelectionsEqual } from "@t3tools/shared/model"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; @@ -1562,7 +1562,7 @@ export const createCodexAdapterV2 = ( const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; const crypto = yield* Crypto.Crypto; const fileSystem = yield* FileSystem.FileSystem; - const hostEnvironment = yield* HostProcessEnvironment; + const hostEnvironment = yield* HostProcess.Environment; const idAllocator = yield* IdAllocator.IdAllocatorV2; const serverConfig = yield* ServerConfig; const homeLayout = yield* resolveCodexHomeLayout(config); @@ -1582,7 +1582,7 @@ export const createCodexAdapterV2 = ( const settings = { ...config, enabled, - binaryPath: expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory), + binaryPath: expandHomePath(config.binaryPath, yield* HostProcess.HomeDirectory), homePath: homeLayout.effectiveHomePath ?? "", } satisfies CodexSettings; @@ -1623,7 +1623,7 @@ const layer: Layer.Layer< const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; const crypto = yield* Crypto.Crypto; const fileSystem = yield* FileSystem.FileSystem; - const hostEnvironment = yield* HostProcessEnvironment; + const hostEnvironment = yield* HostProcess.Environment; const idAllocator = yield* IdAllocator.IdAllocatorV2; const serverConfig = yield* ServerConfig; diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.testkit.ts index daf906ec92cc..4b6b68861c51 100644 --- a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.testkit.ts +++ b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.testkit.ts @@ -1,4 +1,4 @@ -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import type { InteractionUpdate, RunResult } from "@cursor/sdk"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { @@ -519,7 +519,7 @@ export function layer( // Skill discovery also scans user roots under HOME; an empty HOME keeps // replays from picking up the host's own skills. const layerHostEnvironment = Layer.effect( - HostProcessEnvironment, + HostProcess.Environment, Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const home = yield* fs.makeTempDirectoryScoped({ prefix: "t3-cursor-replay-home-" }); diff --git a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts index bdfc883aeb93..1a1391ca9363 100644 --- a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts @@ -7,7 +7,7 @@ import { ThreadId, } from "@t3tools/contracts"; import { GrokSettings } from "@t3tools/provider-grok/settings"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime"; import * as EffectAcpErrors from "effect-acp/errors"; import { xAiRateLimitedErrorCode } from "@t3tools/provider-grok/testing"; @@ -364,7 +364,7 @@ describe("Grok launch permission mode", () => { return launches; }).pipe( // Keep the launch argv unwrapped by the Linux cgroup shim. - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Platform, "darwin"), Effect.provide(layerTest), ); diff --git a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.testkit.ts index b6b67c7a0d56..8d3e86d1ae2a 100644 --- a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.testkit.ts +++ b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.testkit.ts @@ -6,7 +6,7 @@ import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime"; import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost"; @@ -43,7 +43,7 @@ function layerGrokProviderAdapterRegistryReplay( const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const hostPlatform = yield* HostProcessPlatform; + const hostPlatform = yield* HostProcess.Platform; // Same queue the continuation worker drains when the fixture runs it. const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; const replayGate = options.replayGate; diff --git a/apps/server/src/orchestration-v2/workflowScriptQuery.ts b/apps/server/src/orchestration-v2/workflowScriptQuery.ts index a4aa736c669b..ef2ea88c574a 100644 --- a/apps/server/src/orchestration-v2/workflowScriptQuery.ts +++ b/apps/server/src/orchestration-v2/workflowScriptQuery.ts @@ -18,7 +18,7 @@ import * as NodePath from "node:path"; import { OrchestrationGetWorkflowScriptError } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; const SCRIPT_BYTE_CAP = 256 * 1024; @@ -38,7 +38,7 @@ export const readWorkflowScript = Effect.fn("orchestration.readWorkflowScript")( }); } - const home = yield* HostProcessHomeDirectory; + const home = yield* HostProcess.HomeDirectory; const root = yield* Effect.tryPromise({ try: () => NodeFSP.realpath(scriptsRoot(home)), catch: (cause) => diff --git a/apps/server/src/os-jank.ts b/apps/server/src/os-jank.ts index 25c7dfdcb973..38bbcde011cb 100644 --- a/apps/server/src/os-jank.ts +++ b/apps/server/src/os-jank.ts @@ -1,8 +1,4 @@ -import { - HostProcessEnvironment, - HostProcessHomeDirectory, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { listLoginShellCandidates, mergePathEntries, @@ -57,8 +53,8 @@ export const fixPath = Effect.fn("fixPath")(function* (): Effect.fn.Return< never, FileSystem.FileSystem | Path.Path > { - const platform = yield* HostProcessPlatform; - const env = yield* HostProcessEnvironment; + const platform = yield* HostProcess.Platform; + const env = yield* HostProcess.Environment; if (platform === "win32") { const repairedEnvironment = yield* resolveWindowsEnvironment(env).pipe( @@ -97,7 +93,7 @@ export const fixPath = Effect.fn("fixPath")(function* (): Effect.fn.Return< export const expandHomePath = Effect.fn(function* (input: string) { const { join } = yield* Path.Path; - const home = yield* HostProcessHomeDirectory; + const home = yield* HostProcess.HomeDirectory; if (input === "~") { return home; } @@ -110,7 +106,7 @@ export const expandHomePath = Effect.fn(function* (input: string) { export const resolveBaseDir = Effect.fn(function* (raw: string | undefined) { const { join, resolve } = yield* Path.Path; if (!raw || raw.trim().length === 0) { - return join(yield* HostProcessHomeDirectory, ".t3"); + return join(yield* HostProcess.HomeDirectory, ".t3"); } return resolve(yield* expandHomePath(raw.trim())); }); diff --git a/apps/server/src/preview/PortScanner.test.ts b/apps/server/src/preview/PortScanner.test.ts index fa4fd4402e93..8a6bf87d4699 100644 --- a/apps/server/src/preview/PortScanner.test.ts +++ b/apps/server/src/preview/PortScanner.test.ts @@ -6,7 +6,7 @@ import { PREVIEW_URL_MAX_LENGTH, type DiscoveredLocalServer, } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Net from "@t3tools/shared/Net"; import * as Cause from "effect/Cause"; import * as Clock from "effect/Clock"; @@ -74,7 +74,7 @@ const layerProbeFailure = ( reserveLoopbackPort: () => Effect.succeed(40_000), findAvailablePort: (preferred) => Effect.succeed(preferred), }), - Layer.succeed(HostProcessPlatform, "linux"), + Layer.succeed(HostProcess.Platform, "linux"), FetchHttpClient.layer.pipe(Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetch))), ), ), @@ -86,7 +86,7 @@ const layerTestPortDiscovery = PortScanner.layer.pipe( layerNoProc, layerTestProcessRunner, layerTestIntegrationNet, - Layer.succeed(HostProcessPlatform, "win32"), + Layer.succeed(HostProcess.Platform, "win32"), FetchHttpClient.layer, ), ), @@ -122,7 +122,7 @@ const layerLsofScanner = (input: { reserveLoopbackPort: () => Effect.succeed(40_000), findAvailablePort: (preferred) => Effect.succeed(preferred), }), - Layer.succeed(HostProcessPlatform, "linux"), + Layer.succeed(HostProcess.Platform, "linux"), FetchHttpClient.layer.pipe( Layer.provide(Layer.succeed(FetchHttpClient.Fetch, input.fetch)), ), diff --git a/apps/server/src/preview/PortScanner.ts b/apps/server/src/preview/PortScanner.ts index eb573def97e0..9b1d9e966333 100644 --- a/apps/server/src/preview/PortScanner.ts +++ b/apps/server/src/preview/PortScanner.ts @@ -25,7 +25,7 @@ import { ThreadId, type DiscoveredLocalServer, } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Net from "@t3tools/shared/Net"; import { isLoopbackHost, LSOF_LOCAL_HOST_TOKENS } from "@t3tools/shared/preview"; import * as Cause from "effect/Cause"; @@ -342,7 +342,7 @@ export const make = Effect.gen(function* PortDiscoveryMake() { const net = yield* Net.NetService; const processRunner = yield* ProcessRunner.ProcessRunner; const fileSystem = yield* FileSystem.FileSystem; - const hostPlatform = yield* HostProcessPlatform; + const hostPlatform = yield* HostProcess.Platform; const httpClient = (yield* HttpClient.HttpClient).pipe(HttpClient.withScope); const stateRef = yield* Ref.make({ listeners: new Map(), diff --git a/apps/server/src/preview/PreviewBrowser.test.ts b/apps/server/src/preview/PreviewBrowser.test.ts index 6e984c2301e8..32c46fe24ab1 100644 --- a/apps/server/src/preview/PreviewBrowser.test.ts +++ b/apps/server/src/preview/PreviewBrowser.test.ts @@ -1,6 +1,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { expect, it } from "@effect/vitest"; -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Deferred from "effect/Deferred"; import * as Crypto from "effect/Crypto"; import type * as Duration from "effect/Duration"; @@ -20,7 +20,7 @@ import * as PreviewBrowser from "./PreviewBrowser.ts"; // POSIX modes cannot be checked on NTFS, so follow the host like the Antigravity suite. const hostPlatform: NodeJS.Platform = - HostProcessPlatform.defaultValue() === "win32" ? "win32" : "linux"; + HostProcess.Platform.defaultValue() === "win32" ? "win32" : "linux"; const executableName = hostPlatform === "win32" ? "chrome-headless-shell.exe" : "chrome-headless-shell"; const ROOT = "chrome-headless-shell-fixture/"; @@ -105,8 +105,8 @@ const makeHarness = Effect.fn("test.makePreviewBrowser")(function* ( }, ...(options.wait === undefined ? {} : { wait: options.wait }), }).pipe( - Effect.provideService(HostProcessPlatform, hostPlatform), - Effect.provideService(HostProcessArchitecture, "x64"), + Effect.provideService(HostProcess.Platform, hostPlatform), + Effect.provideService(HostProcess.Architecture, "x64"), Effect.provideService( HttpClient.HttpClient, HttpClient.make((request) => diff --git a/apps/server/src/preview/PreviewBrowser.ts b/apps/server/src/preview/PreviewBrowser.ts index 8b64861ecbbb..1525bcde44cf 100644 --- a/apps/server/src/preview/PreviewBrowser.ts +++ b/apps/server/src/preview/PreviewBrowser.ts @@ -1,6 +1,6 @@ // @effect-diagnostics nodeBuiltinImport:off - Effect has no incremental digest. import * as EffectNodeStream from "@effect/platform-node/NodeStream"; -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Cause from "effect/Cause"; import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; @@ -175,8 +175,8 @@ export const makePreviewBrowser = Effect.fn("PreviewBrowser.make")(function* ( const http = yield* HttpClient.HttpClient; // Installs belong to the service, so they finish even when no caller is still waiting. const serviceScope = yield* Effect.scope; - const platform = yield* HostProcessPlatform; - const arch = yield* HostProcessArchitecture; + const platform = yield* HostProcess.Platform; + const arch = yield* HostProcess.Architecture; const release = options.release === undefined ? previewBrowserRelease(platform, arch) : options.release; const wait = options.wait ?? "45 seconds"; diff --git a/apps/server/src/preview/PreviewBrowserHost.test.ts b/apps/server/src/preview/PreviewBrowserHost.test.ts index 8fcfddff5cb5..5ae0a70752ca 100644 --- a/apps/server/src/preview/PreviewBrowserHost.test.ts +++ b/apps/server/src/preview/PreviewBrowserHost.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "@effect/vitest"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; @@ -67,7 +67,7 @@ const diagnose = (input: { setupCommand: "sudo t3 browser setup", }).pipe( Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), - Effect.provideService(HostProcessPlatform, input.platform), + Effect.provideService(HostProcess.Platform, input.platform), Effect.map((error) => ({ error, commands })), ); }; diff --git a/apps/server/src/preview/PreviewBrowserHost.ts b/apps/server/src/preview/PreviewBrowserHost.ts index 9ea56c4fa18b..de136cf41dad 100644 --- a/apps/server/src/preview/PreviewBrowserHost.ts +++ b/apps/server/src/preview/PreviewBrowserHost.ts @@ -7,7 +7,7 @@ * gets the command instead, and only the operator's explicit * `T3CODE_SERVER_BROWSER_SANDBOX=0` launches without it. */ -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Schema from "effect/Schema"; @@ -108,7 +108,7 @@ export const diagnoseLaunchFailure = Effect.fn("PreviewBrowserHost.diagnoseLaunc if (input.output.includes(NO_SANDBOX_SIGNATURE)) { return new PreviewBrowserSandboxError({ setupCommand }); } - if ((yield* HostProcessPlatform) !== "linux") return undefined; + if ((yield* HostProcess.Platform) !== "linux") return undefined; const libraries = yield* missingLibraries(input.executable); return libraries.length === 0 ? undefined @@ -147,7 +147,7 @@ export const missingLibraries = Effect.fn("PreviewBrowserHost.missingLibraries") * root, so the server checks it at startup. */ export const sandboxBlocked = Effect.gen(function* () { - if ((yield* HostProcessPlatform) !== "linux") return false; + if ((yield* HostProcess.Platform) !== "linux") return false; const fs = yield* FileSystem.FileSystem; const restricted = yield* fs.readFileString(USERNS_RESTRICTION).pipe( Effect.map((value) => value.trim() === "1"), diff --git a/apps/server/src/preview/ServerBrowser.ts b/apps/server/src/preview/ServerBrowser.ts index f3d9bea8b40f..072a022f90c0 100644 --- a/apps/server/src/preview/ServerBrowser.ts +++ b/apps/server/src/preview/ServerBrowser.ts @@ -42,11 +42,7 @@ import { SERVER_BROWSER_AUTOMATION_CLIENT_ID, type PreviewAppearancePreference, } from "@t3tools/contracts"; -import { - HostProcessArchitecture, - HostProcessEnvironment, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { normalizePreviewUrl } from "@t3tools/shared/preview"; import { resolvePreviewViewport } from "@t3tools/shared/previewViewport"; import * as NodeCrypto from "node:crypto"; @@ -476,7 +472,7 @@ const CLIPBOARD_SCRIPT = `(() => { const make = Effect.gen(function* () { const config = yield* ServerConfig.ServerConfig; - const host = { platform: yield* HostProcessPlatform, arch: yield* HostProcessArchitecture }; + const host = { platform: yield* HostProcess.Platform, arch: yield* HostProcess.Architecture }; const manager = yield* PreviewManager.PreviewManager; const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker; const environment = yield* ServerEnvironment.ServerEnvironment; @@ -2367,7 +2363,7 @@ const make = Effect.gen(function* () { yield* manager.events.pipe(Stream.runForEach(mirrorManagerEvent), Effect.forkScoped); // Whoever runs the server learns the fix before anyone opens a tab. if ( - !PreviewBrowserHost.sandboxDisabled(yield* HostProcessEnvironment) && + !PreviewBrowserHost.sandboxDisabled(yield* HostProcess.Environment) && (yield* PreviewBrowserHost.sandboxBlocked) ) { yield* Effect.logWarning( diff --git a/apps/server/src/process/externalLauncher.test.ts b/apps/server/src/process/externalLauncher.test.ts index cd83cad6e968..6df4ea16db88 100644 --- a/apps/server/src/process/externalLauncher.test.ts +++ b/apps/server/src/process/externalLauncher.test.ts @@ -17,7 +17,7 @@ import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { SpawnExecutableResolution } from "@t3tools/shared/shell"; import * as ExternalLauncher from "./externalLauncher.ts"; @@ -25,7 +25,7 @@ import * as ExternalLauncher from "./externalLauncher.ts"; // directory to a posix-mocked resolver as PATH. On a Windows host the temp // path carries a drive letter, so the posix `:` split shatters it; there is // no posix executable to find there anyway. -const windowsHost = HostProcessPlatform.defaultValue() === "win32"; +const windowsHost = HostProcess.Platform.defaultValue() === "win32"; interface MockSpawnResult { readonly exitCode?: number; @@ -85,7 +85,7 @@ const layerTest = (input: { return Layer.mergeAll( ExternalLauncher.layer.pipe(Layer.provide(Layer.merge(NodeServices.layer, layerSpawner))), - Layer.succeed(HostProcessPlatform, input.platform), + Layer.succeed(HostProcess.Platform, input.platform), Layer.succeed( SpawnExecutableResolution, (command) => input.resolveExecutable?.(command) ?? command, @@ -1302,7 +1302,7 @@ it.effect("memoizes editor discovery and refreshes after the cache window", () = Effect.provide( Layer.mergeAll( layerLauncher, - Layer.succeed(HostProcessPlatform, "win32"), + Layer.succeed(HostProcess.Platform, "win32"), ConfigProvider.layer( ConfigProvider.fromEnv({ env: { @@ -1367,7 +1367,7 @@ it.effect("keeps scanning after the caller is interrupted and shares that scan", Effect.provide( Layer.mergeAll( layerLauncher, - Layer.succeed(HostProcessPlatform, "win32"), + Layer.succeed(HostProcess.Platform, "win32"), ConfigProvider.layer( ConfigProvider.fromEnv({ env: { diff --git a/apps/server/src/process/externalLauncher.ts b/apps/server/src/process/externalLauncher.ts index 6215f8653ffc..74e95b9ddcc4 100644 --- a/apps/server/src/process/externalLauncher.ts +++ b/apps/server/src/process/externalLauncher.ts @@ -19,7 +19,7 @@ import { type LaunchEditorInput, } from "@t3tools/contracts"; import { resolveEditorCommand } from "@t3tools/shared/editor"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { isCommandAvailable, resolveSpawnCommand, @@ -441,20 +441,20 @@ const buildAvailableEditors = Effect.fn("externalLauncher.buildAvailableEditors" const resolveBrowserLaunch = Effect.fn("externalLauncher.resolveBrowserLaunch")(function* ( target: string, ) { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const env = yield* readBrowserLaunchEnv; return buildBrowserLaunch(target, platform, env); }); const resolveAvailableEditors = Effect.fn("externalLauncher.resolveAvailableEditors")(function* () { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const env = { ...(yield* readBrowserLaunchEnv), ...(yield* readCommandLookupEnv) }; return yield* buildAvailableEditors(platform, env).pipe(withPathDirectoryListings); }); const resolveFileManagerRevealKind = Effect.fn("externalLauncher.resolveFileManagerRevealKind")( function* () { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const env = { ...(yield* readBrowserLaunchEnv), ...(yield* readCommandLookupEnv) }; return yield* fileManagerRevealKindForPlatform(platform, env); }, @@ -519,7 +519,7 @@ const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* ( ExternalLauncherError, FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner > { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const env = { ...(yield* readBrowserLaunchEnv), ...(yield* readCommandLookupEnv) }; yield* Effect.annotateCurrentSpan({ "externalLauncher.editor": input.editor, diff --git a/apps/server/src/processRunner.test.ts b/apps/server/src/processRunner.test.ts index d241b3b3a4d4..3bec75351baf 100644 --- a/apps/server/src/processRunner.test.ts +++ b/apps/server/src/processRunner.test.ts @@ -9,7 +9,7 @@ import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import { TestClock } from "effect/testing"; import { ChildProcessSpawner } from "effect/process"; -import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { SpawnExecutableResolution } from "@t3tools/shared/shell"; import * as ProcessRunner from "./processRunner.ts"; @@ -145,8 +145,8 @@ describe("runProcess", () => { args: ["repos", "pr", "list", "--source-branch", "feature & release"], env: { AZURE_CONFIG_DIR: "C:\\Users\\tester\\.azure" }, }).pipe( - Effect.provideService(HostProcessPlatform, "win32"), - Effect.provideService(HostProcessEnvironment, { + Effect.provideService(HostProcess.Platform, "win32"), + Effect.provideService(HostProcess.Environment, { PATH: "C:\\Users\\tester\\AppData\\Roaming\\npm", PATHEXT: ".COM;.EXE;.BAT;.CMD", }), @@ -407,7 +407,7 @@ describe("isWindowsCommandNotFound", () => { const isCommandNotFound = yield* ProcessRunner.isWindowsCommandNotFound( 1, "wird nicht als interner oder externer Befehl, betriebsfahiges Programm oder Batch-Datei erkannt", - ).pipe(Effect.provideService(HostProcessPlatform, "win32")); + ).pipe(Effect.provideService(HostProcess.Platform, "win32")); expect(isCommandNotFound).toBe(true); }), ); diff --git a/apps/server/src/project/AgentSessionScanner.ts b/apps/server/src/project/AgentSessionScanner.ts index 5177a948d945..454971a75ab8 100644 --- a/apps/server/src/project/AgentSessionScanner.ts +++ b/apps/server/src/project/AgentSessionScanner.ts @@ -44,11 +44,7 @@ import { parseGitHubRepositoryNameWithOwnerFromRemoteUrl, parseOriginUrlFromGitConfig, } from "@t3tools/shared/git"; -import { - HostProcessEnvironment, - HostProcessHomeDirectory, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { normalizeProjectPathForComparison } from "@t3tools/shared/path"; import * as ServerConfig from "../config.ts"; @@ -633,9 +629,9 @@ export const make = Effect.gen(function* () { const worktreesDir = path.resolve(serverConfig.worktreesDir); // Windows filesystems are case-insensitive, so path prefix checks there // must case fold. - const foldWorktreeCase = (yield* HostProcessPlatform) === "win32"; - const hostEnvironment = yield* HostProcessEnvironment; - const homeDir = yield* HostProcessHomeDirectory; + const foldWorktreeCase = (yield* HostProcess.Platform) === "win32"; + const hostEnvironment = yield* HostProcess.Environment; + const homeDir = yield* HostProcess.HomeDirectory; // `/private/tmp` is what macOS reports for sessions started in `/tmp`. const excludedProjectRoots = new Set( [homeDir, NodeOS.tmpdir(), "/tmp", "/private/tmp"].map((directory) => diff --git a/apps/server/src/project/ProjectSetupScriptRunner.ts b/apps/server/src/project/ProjectSetupScriptRunner.ts index 973617156277..c96f88087865 100644 --- a/apps/server/src/project/ProjectSetupScriptRunner.ts +++ b/apps/server/src/project/ProjectSetupScriptRunner.ts @@ -1,5 +1,5 @@ import { ProjectId, type ProjectScript } from "@t3tools/contracts"; -import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { projectScriptRuntimeEnv, resolveProjectScripts, @@ -206,8 +206,8 @@ export const make = Effect.gen(function* () { const serverSettings = yield* ServerSettings.ServerSettingsService; const crypto = yield* Crypto.Crypto; const completionShell = resolveCompletionShell( - yield* HostProcessPlatform, - yield* HostProcessEnvironment, + yield* HostProcess.Platform, + yield* HostProcess.Environment, ); /** diff --git a/apps/server/src/provider/AntigravityInstallation.test.ts b/apps/server/src/provider/AntigravityInstallation.test.ts index ac256ef3dcd0..e99c2f684954 100644 --- a/apps/server/src/provider/AntigravityInstallation.test.ts +++ b/apps/server/src/provider/AntigravityInstallation.test.ts @@ -1,11 +1,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { expect, it } from "@effect/vitest"; -import { - HostProcessArchitecture, - HostProcessEnvironment, - HostProcessIsExecutable, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Crypto from "effect/Crypto"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; @@ -62,7 +57,7 @@ const zipFixtures = { // the host and let the fixture names follow; the suite is about install // mechanics, which are the same on every platform. const hostPlatform: NodeJS.Platform = - HostProcessPlatform.defaultValue() === "win32" ? "win32" : "linux"; + HostProcess.Platform.defaultValue() === "win32" ? "win32" : "linux"; const completeArchive = Buffer.from( hostPlatform === "win32" ? zipFixtures.windows : zipFixtures.complete, "base64", @@ -189,9 +184,9 @@ const makeHarness = Effect.fn("test.makeAntigravityInstallation")(function* ( }), }).pipe( Effect.provideService(FileSystem.FileSystem, trackedFs), - Effect.provideService(HostProcessPlatform, platform), - Effect.provideService(HostProcessArchitecture, "x64"), - Effect.provideService(HostProcessEnvironment, { PATH: options.path ?? "" }), + Effect.provideService(HostProcess.Platform, platform), + Effect.provideService(HostProcess.Architecture, "x64"), + Effect.provideService(HostProcess.Environment, { PATH: options.path ?? "" }), Effect.provideService( HttpClient.HttpClient, HttpClient.make((request) => @@ -259,7 +254,7 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { }); expect(requests).toEqual([]); expect(validations).toEqual([]); - }).pipe(Effect.provideService(HostProcessIsExecutable, true)), + }).pipe(Effect.provideService(HostProcess.IsExecutable, true)), ); it.effect("verifies both files before activating a streamed download", () => @@ -734,7 +729,7 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { // Real posix executables in a real temp dir, resolved by a linux-mocked // PATH walk; a Windows temp path cannot be split on `:`. - it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")( + it.effect.skipIf(HostProcess.Platform.defaultValue() === "win32")( "honors explicit paths and reports invalid overrides without falling back", () => Effect.gen(function* () { diff --git a/apps/server/src/provider/AntigravityInstallation.ts b/apps/server/src/provider/AntigravityInstallation.ts index 4f78ee8ebb33..c57200f4afda 100644 --- a/apps/server/src/provider/AntigravityInstallation.ts +++ b/apps/server/src/provider/AntigravityInstallation.ts @@ -1,11 +1,7 @@ // @effect-diagnostics nodeBuiltinImport:off - Effect has no incremental digest or free-space query. import * as EffectNodeStream from "@effect/platform-node/NodeStream"; import { ProviderDriverKind, type ProviderInstallState } from "@t3tools/contracts"; -import { - HostProcessArchitecture, - HostProcessEnvironment, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveNodeExecutable, nodeRuntimeUnavailableMessage } from "@t3tools/shared/nodeRuntime"; import * as Clock from "effect/Clock"; import * as Cause from "effect/Cause"; @@ -156,9 +152,9 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma const http = yield* HttpClient.HttpClient; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const serviceScope = yield* Effect.scope; - const platform = yield* HostProcessPlatform; - const arch = yield* HostProcessArchitecture; - const environment = yield* HostProcessEnvironment; + const platform = yield* HostProcess.Platform; + const arch = yield* HostProcess.Architecture; + const environment = yield* HostProcess.Environment; const releaseAsset = options.releaseAsset === undefined ? resolveAntigravityReleaseAsset(platform, arch) @@ -410,7 +406,7 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma yield* resolveNodeExecutable("Antigravity", environment).pipe( Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path), - Effect.provideService(HostProcessPlatform, platform), + Effect.provideService(HostProcess.Platform, platform), Effect.mapError((cause) => installationError("verify", nodeRuntimeUnavailableMessage("Antigravity"), cause), ), diff --git a/apps/server/src/provider/CodexInstallation.test.ts b/apps/server/src/provider/CodexInstallation.test.ts index 88bc67690125..abc67ad55ea1 100644 --- a/apps/server/src/provider/CodexInstallation.test.ts +++ b/apps/server/src/provider/CodexInstallation.test.ts @@ -1,11 +1,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import * as ModelManifest from "./ModelManifest.ts"; import { expect, it } from "@effect/vitest"; -import { - HostProcessArchitecture, - HostProcessEnvironment, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Crypto from "effect/Crypto"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; @@ -66,9 +62,9 @@ const makeHarness = Effect.fn("test.makeCodexInstallation")(function* ( forceRefresh: Effect.succeed(ModelManifest.BUNDLED_MODEL_MANIFEST), refreshInBackground: Effect.void, }), - Effect.provideService(HostProcessPlatform, "darwin"), - Effect.provideService(HostProcessArchitecture, "arm64"), - Effect.provideService(HostProcessEnvironment, { PATH: input.local ? localDirectory : "" }), + Effect.provideService(HostProcess.Platform, "darwin"), + Effect.provideService(HostProcess.Architecture, "arm64"), + Effect.provideService(HostProcess.Environment, { PATH: input.local ? localDirectory : "" }), Effect.provideService( HttpClient.HttpClient, HttpClient.make((request) => diff --git a/apps/server/src/provider/CodexInstallation.ts b/apps/server/src/provider/CodexInstallation.ts index 7dfce2bba433..fa7876b1e92a 100644 --- a/apps/server/src/provider/CodexInstallation.ts +++ b/apps/server/src/provider/CodexInstallation.ts @@ -1,10 +1,6 @@ // @effect-diagnostics nodeBuiltinImport:off - Effect has no incremental digest. import { ProviderDriverKind, type ProviderInstallState } from "@t3tools/contracts"; -import { - HostProcessArchitecture, - HostProcessEnvironment, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveCommandPath, resolveSpawnCommand } from "@t3tools/shared/shell"; import * as Clock from "effect/Clock"; import * as Cause from "effect/Cause"; @@ -169,9 +165,9 @@ export const makeCodexInstallation = Effect.fn("makeCodexInstallation")(function const http = yield* HttpClient.HttpClient; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const serviceScope = yield* Effect.scope; - const platform = yield* HostProcessPlatform; - const environment = yield* HostProcessEnvironment; - const arch = yield* HostProcessArchitecture; + const platform = yield* HostProcess.Platform; + const environment = yield* HostProcess.Environment; + const arch = yield* HostProcess.Architecture; const asset = options.releaseAsset === undefined ? resolveCodexReleaseAsset(platform, arch) @@ -311,7 +307,7 @@ export const makeCodexInstallation = Effect.fn("makeCodexInstallation")(function args: ReadonlyArray, ) { const resolved = yield* resolveSpawnCommand(command, args).pipe( - Effect.provideService(HostProcessPlatform, platform), + Effect.provideService(HostProcess.Platform, platform), ); const child = yield* spawner.spawn( ChildProcess.make(resolved.command, resolved.args, { shell: resolved.shell }), @@ -335,7 +331,7 @@ export const makeCodexInstallation = Effect.fn("makeCodexInstallation")(function const resolveLocal = Effect.fn("CodexInstallation.resolveLocal")( function* () { const executablePath = yield* resolveCommandPath("codex", { env: environment }).pipe( - Effect.provideService(HostProcessPlatform, platform), + Effect.provideService(HostProcess.Platform, platform), Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path), ); diff --git a/apps/server/src/provider/CodexProvider.ts b/apps/server/src/provider/CodexProvider.ts index a8895e21ae32..cb1a5513cd01 100644 --- a/apps/server/src/provider/CodexProvider.ts +++ b/apps/server/src/provider/CodexProvider.ts @@ -47,7 +47,7 @@ import { type CodexRateLimitSnapshot, type CodexResetCreditsSummary, } from "./codexUsageLimits.ts"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import packageJson from "../../package.json" with { type: "json" }; const isCodexAppServerSpawnError = Schema.is(CodexErrors.CodexAppServerSpawnError); const RATE_LIMITS_PROBE_TIMEOUT_MS = 3_000; @@ -376,7 +376,7 @@ export const withCodexAppServerClient = Effect.fn("withCodexAppServerClient")(fu // "CODEX_HOME points to '~/.codex_work', but that path does not exist". // Expand here for parity with `CodexTextGeneration`. const resolvedHomePath = input.homePath - ? expandHomePath(input.homePath, yield* HostProcessHomeDirectory) + ? expandHomePath(input.homePath, yield* HostProcess.HomeDirectory) : undefined; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const environment = { diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.test.ts b/apps/server/src/provider/Drivers/AntigravityDriver.test.ts index dc0bdd4d756f..f15cdfeff676 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.test.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.test.ts @@ -5,12 +5,7 @@ import { ProviderInstanceId, type AntigravitySettings, } from "@t3tools/contracts"; -import { - HostProcessEnvironment, - HostProcessExecutablePath, - HostProcessIsExecutable, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as Deferred from "effect/Deferred"; import * as Fiber from "effect/Fiber"; @@ -37,7 +32,7 @@ import { AntigravityDriver } from "./AntigravityDriver.ts"; import * as ProviderHostLive from "../ProviderHostLive.ts"; import * as ServerSecretStore from "../../auth/ServerSecretStore.ts"; -const hostPlatform = HostProcessPlatform.defaultValue(); +const hostPlatform = HostProcess.Platform.defaultValue(); const windowsHost = hostPlatform === "win32"; const decodeRequest = Schema.decodeEffect( Schema.fromJsonString( @@ -65,8 +60,8 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( const path = yield* Path.Path; const config = yield* ServerConfig.ServerConfig; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const nodePath = yield* HostProcessExecutablePath; - const baseEnv = yield* HostProcessEnvironment; + const nodePath = yield* HostProcess.ExecutablePath; + const baseEnv = yield* HostProcess.Environment; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-antigravity-driver-" }); const instanceId = ProviderInstanceId.make(path.basename(root)); const mockAgentPath = yield* path.fromFileUrl( @@ -283,8 +278,8 @@ it.layer(layerTest)("AntigravityDriver", (it) => { expect(h.launches).toEqual([]); }).pipe( Effect.scoped, - Effect.provideService(HostProcessIsExecutable, true), - Effect.provideService(HostProcessEnvironment, { PATH: "" }), + Effect.provideService(HostProcess.IsExecutable, true), + Effect.provideService(HostProcess.Environment, { PATH: "" }), ), ); diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.ts b/apps/server/src/provider/Drivers/AntigravityDriver.ts index cdf938dbb4d0..c82565e8f2fe 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.ts @@ -1,7 +1,7 @@ import { withAgentDeviceEnvironment } from "@t3tools/provider-core/server/mcpSession"; import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; import { AntigravitySettings, ProviderDriverKind, ProviderSetupError } from "@t3tools/contracts"; -import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime"; import { NodeRuntimeUnavailableError, @@ -115,9 +115,9 @@ export const AntigravityDriver: ProviderDriver< const authConfigIssue = antigravityAuthConfigIssue(auth); const processEnvironment = yield* mergeProviderInstanceEnvironment(environment); const userHome = resolveAntigravityUserHome( - yield* HostProcessPlatform, + yield* HostProcess.Platform, processEnvironment, - yield* HostProcessHomeDirectory, + yield* HostProcess.HomeDirectory, ); const directories = yield* resolveAntigravityInstanceDirectories( host.paths.stateDir, diff --git a/apps/server/src/provider/Drivers/AntigravityUsage.ts b/apps/server/src/provider/Drivers/AntigravityUsage.ts index d99539b555c1..ee5369b21716 100644 --- a/apps/server/src/provider/Drivers/AntigravityUsage.ts +++ b/apps/server/src/provider/Drivers/AntigravityUsage.ts @@ -17,7 +17,7 @@ import type { ProviderUsageReader, ProviderUsageScan, } from "@t3tools/provider-core/server/usage"; -import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; @@ -44,12 +44,12 @@ const make = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const host = yield* ProviderHost.ProviderHost; - const hostEnvironment = yield* HostProcessEnvironment; + const hostEnvironment = yield* HostProcess.Environment; const cache = makeAntigravityUsageCache(); /** `ANTIGRAVITY_DATA_DIR` (comma-separated) or the defaults, canonicalized. */ const dataRoots = Effect.gen(function* () { - const home = yield* HostProcessHomeDirectory; + const home = yield* HostProcess.HomeDirectory; const configured = hostEnvironment["ANTIGRAVITY_DATA_DIR"] ?.split(",") .map((value) => value.trim()) diff --git a/apps/server/src/provider/Drivers/ClaudeDriver.ts b/apps/server/src/provider/Drivers/ClaudeDriver.ts index 9a43be65aac4..be7872a7df28 100644 --- a/apps/server/src/provider/Drivers/ClaudeDriver.ts +++ b/apps/server/src/provider/Drivers/ClaudeDriver.ts @@ -71,7 +71,7 @@ import { makeClaudeContinuationGroupKey, resolveClaudeHomePath, } from "./ClaudeHome.ts"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings); const DRIVER_KIND = ProviderDriverKind.make("claudeAgent"); @@ -139,7 +139,7 @@ export const ClaudeDriver: ProviderDriver(effect: Effect.Effect) => effect.pipe( - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), Effect.provideService(SpawnExecutableResolution, () => input.resolvedCommand), Effect.provideService(ClaudeExecutableFileCheck, (filePath) => existing.has(filePath)), ); @@ -28,7 +28,7 @@ describe("resolveClaudeSdkExecutablePath", () => { Effect.gen(function* () { expect( yield* resolveClaudeSdkExecutablePath("claude", {}).pipe( - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Platform, "darwin"), Effect.provideService(SpawnExecutableResolution, () => { throw new Error("must not resolve on non-Windows platforms"); }), diff --git a/apps/server/src/provider/Drivers/ClaudeExecutable.ts b/apps/server/src/provider/Drivers/ClaudeExecutable.ts index fe1192d1df47..a7d409b84d9b 100644 --- a/apps/server/src/provider/Drivers/ClaudeExecutable.ts +++ b/apps/server/src/provider/Drivers/ClaudeExecutable.ts @@ -2,7 +2,7 @@ import * as NodeFS from "node:fs"; import * as NodePath from "node:path"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { SpawnExecutableResolution } from "@t3tools/shared/shell"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; @@ -67,7 +67,7 @@ export const ClaudeExecutableFileCheck = Context.Reference( */ export const resolveClaudeSdkExecutablePath = Effect.fn("resolveClaudeSdkExecutablePath")( function* (binaryPath: string, environment: NodeJS.ProcessEnv): Effect.fn.Return { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; if (platform !== "win32") { return binaryPath; } diff --git a/apps/server/src/provider/Drivers/ClaudeHome.ts b/apps/server/src/provider/Drivers/ClaudeHome.ts index 7ebfffa3fddb..78bb34c2fd9e 100644 --- a/apps/server/src/provider/Drivers/ClaudeHome.ts +++ b/apps/server/src/provider/Drivers/ClaudeHome.ts @@ -4,7 +4,7 @@ import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; const quotePath = Schema.encodeSync(Schema.fromJsonString(Schema.String)); @@ -20,7 +20,7 @@ export const resolveClaudeHomePath = Effect.fn("resolveClaudeHomePath")(function environment?: NodeJS.ProcessEnv, ): Effect.fn.Return { const path = yield* Path.Path; - const home = yield* HostProcessHomeDirectory; + const home = yield* HostProcess.HomeDirectory; const homePath = config.homePath.trim(); if (homePath.length > 0) { return path.resolve(expandHomePath(homePath, home)); diff --git a/apps/server/src/provider/Drivers/ClaudeSkills.ts b/apps/server/src/provider/Drivers/ClaudeSkills.ts index 3082f53b0280..40ab84d65ed6 100644 --- a/apps/server/src/provider/Drivers/ClaudeSkills.ts +++ b/apps/server/src/provider/Drivers/ClaudeSkills.ts @@ -19,7 +19,7 @@ import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; -import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { fromLenientJson } from "@t3tools/shared/schemaJson"; import { parse as parseYamlDocument } from "yaml"; @@ -242,7 +242,7 @@ const readSkillOverrides = Effect.fn("readSkillOverrides")(function* ( ): Effect.fn.Return, never, FileSystem.FileSystem | Path.Path> { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const overridesByName = new Map(); const repositoryRoot = cwd === undefined ? undefined : yield* findRepositoryRoot(cwd); @@ -295,7 +295,7 @@ const resolveClaudeConfigDirPath = Effect.fn("resolveClaudeConfigDirPath")(funct cwd?: string, ): Effect.fn.Return { const path = yield* Path.Path; - const home = yield* HostProcessHomeDirectory; + const home = yield* HostProcess.HomeDirectory; const homePath = config.homePath.trim(); if (homePath.length > 0) { return path.resolve(expandHomePath(homePath, home)); diff --git a/apps/server/src/provider/Drivers/CodexDriver.test.ts b/apps/server/src/provider/Drivers/CodexDriver.test.ts index 54053e1dc9b8..7ac25a2e6bc1 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.test.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.test.ts @@ -7,7 +7,7 @@ import * as NodeOS from "node:os"; import * as NodePath from "node:path"; import { expect, it } from "@effect/vitest"; import { EnvironmentId, ProviderInstanceId, ProviderSessionId, ThreadId } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as Deferred from "effect/Deferred"; import * as Fiber from "effect/Fiber"; @@ -88,7 +88,7 @@ const layerDeps = ServerConfig.layerTest(process.cwd(), { const layerTest = ProviderHostLive.layer.pipe(Layer.provideMerge(layerDeps)); // The `#!/bin/sh` stub below cannot be resolved as an executable on Windows. -const windowsHost = HostProcessPlatform.defaultValue() === "win32"; +const windowsHost = HostProcess.Platform.defaultValue() === "win32"; const noSpawn = ChildProcessSpawner.make(() => Effect.die("Disabled Codex must not spawn a process"), diff --git a/apps/server/src/provider/Drivers/CodexDriver.ts b/apps/server/src/provider/Drivers/CodexDriver.ts index e1fadbcd175e..8aa546d103e1 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.ts @@ -77,7 +77,7 @@ import { codexUsageReader } from "./codexUsage.ts"; import * as CodexInstallation from "../CodexInstallation.ts"; import * as ServerSecretStore from "../../auth/ServerSecretStore.ts"; import * as ServerEnvironment from "../../environment/ServerEnvironment.ts"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; const decodeCodexSettings = Schema.decodeSync(CodexSettings); const DRIVER_KIND = ProviderDriverKind.make("codex"); @@ -179,7 +179,7 @@ export const CodexDriver: ProviderDriver { const path = yield* Path.Path; - const home = yield* HostProcessHomeDirectory; + const home = yield* HostProcess.HomeDirectory; const sharedHomePath = resolveHomePath(path, home, config.homePath); const shadowHomePath = config.shadowHomePath.trim(); if (shadowHomePath.length === 0) { diff --git a/apps/server/src/provider/Drivers/claudeUsage.ts b/apps/server/src/provider/Drivers/claudeUsage.ts index 38df3146e097..2bab4f5e92b6 100644 --- a/apps/server/src/provider/Drivers/claudeUsage.ts +++ b/apps/server/src/provider/Drivers/claudeUsage.ts @@ -17,7 +17,7 @@ import { type TranscriptUsageFormat, type UsageRecord, } from "@t3tools/provider-core/server/usage"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; /** * Parses one line of a Claude Code transcript. @@ -111,7 +111,7 @@ export const claudeUsageReader: ProviderUsageReader = // An undecodable config has no trustworthy home to read. if (config === undefined) return []; const path = yield* Path.Path; - const homeDirectory = yield* HostProcessHomeDirectory; + const homeDirectory = yield* HostProcess.HomeDirectory; const configured = config.homePath.trim(); const home = configured ? expandHomePath(configured, homeDirectory) diff --git a/apps/server/src/provider/ProviderInstanceRegistry.test.ts b/apps/server/src/provider/ProviderInstanceRegistry.test.ts index 7c6b191a8b84..49d575f7eb66 100644 --- a/apps/server/src/provider/ProviderInstanceRegistry.test.ts +++ b/apps/server/src/provider/ProviderInstanceRegistry.test.ts @@ -39,11 +39,7 @@ import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderL import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; import type { GrokSettings } from "@t3tools/provider-grok/settings"; import type { CursorSettings } from "@t3tools/provider-cursor/settings"; -import { - HostProcessHomeDirectory, - HostProcessPlatform, - isHostWindows, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -156,7 +152,7 @@ const makeTildeProviderFixtures = Effect.fn( )(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const homePath = yield* HostProcessHomeDirectory; + const homePath = yield* HostProcess.HomeDirectory; const fixtureDir = yield* fileSystem.makeTempDirectoryScoped({ directory: homePath, prefix: ".t3-provider-path-test-", @@ -349,7 +345,7 @@ describe("ProviderInstanceRegistry — multi-instance codex slice", () => { it.live("reports Codex's answer when a redemption changed nothing", () => Effect.gen(function* () { - if (yield* isHostWindows) return; + if (yield* HostProcess.isWindows) return; const fileSystem = yield* FileSystem.FileSystem; const fixtures = yield* makeTildeProviderFixtures(); yield* fileSystem.writeFileString( @@ -386,7 +382,7 @@ describe("ProviderInstanceRegistry — multi-instance codex slice", () => { it.live("runs Codex and Claude readiness probes from configured tilde paths", () => Effect.gen(function* () { - if (yield* isHostWindows) return; + if (yield* HostProcess.isWindows) return; const fixtures = yield* makeTildeProviderFixtures(); @@ -503,7 +499,7 @@ describe("ProviderInstanceRegistry — multi-instance codex slice", () => { return { outcome, after: yield* instance!.snapshot.getSnapshot }; }).pipe( // macOS logins live in the Keychain, where resets are never read. - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), Effect.provide(layerTest), ); diff --git a/apps/server/src/provider/acp/AcpSessionRuntime.processTree.test.ts b/apps/server/src/provider/acp/AcpSessionRuntime.processTree.test.ts index 13a7dd6c2c81..81d499733caf 100644 --- a/apps/server/src/provider/acp/AcpSessionRuntime.processTree.test.ts +++ b/apps/server/src/provider/acp/AcpSessionRuntime.processTree.test.ts @@ -5,7 +5,7 @@ import * as NodePath from "node:path"; import * as NodeURL from "node:url"; import { describe, expect, it, vi } from "@effect/vitest"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Clock from "effect/Clock"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; @@ -155,7 +155,7 @@ describe("terminatePosixOwnedProcessTree", () => { it.live("preserves exact argv and strips wrapper-only environment before exec", () => Effect.gen(function* () { - if ((yield* HostProcessPlatform) !== "linux") return; + if ((yield* HostProcess.Platform) !== "linux") return; const lease = AcpSessionRuntime.makeLinuxCgroupController().create(); if (lease === undefined) return; const scratchRoot = NodePath.join(process.cwd(), "tmp"); @@ -235,7 +235,7 @@ describe("terminatePosixOwnedProcessTree", () => { ); it("contains a packaged-runtime command without requiring cgroup delegation", () => { - if (HostProcessPlatform.defaultValue() !== "linux") return; + if (HostProcess.Platform.defaultValue() !== "linux") return; const scratchRoot = NodePath.join(process.cwd(), "tmp"); NodeFS.mkdirSync(scratchRoot, { recursive: true }); const scratch = NodeFS.mkdtempSync(NodePath.join(scratchRoot, "acp-cgroup-wrapper-fake-")); @@ -316,7 +316,7 @@ describe("terminatePosixOwnedProcessTree", () => { it.live("kills a post-TERM detached double fork without touching an unrelated sentinel", () => Effect.gen(function* () { - if ((yield* HostProcessPlatform) !== "linux") return; + if ((yield* HostProcess.Platform) !== "linux") return; const controller = AcpSessionRuntime.makeLinuxCgroupController(); const lease = controller.create(); if (lease === undefined) return; @@ -531,7 +531,7 @@ describe("terminatePosixOwnedProcessTree", () => { it.live("finds a child forked by a non-leader pthread", () => Effect.gen(function* () { - if ((yield* HostProcessPlatform) !== "linux") return; + if ((yield* HostProcess.Platform) !== "linux") return; const scratchRoot = NodePath.join(process.cwd(), "tmp"); NodeFS.mkdirSync(scratchRoot, { recursive: true }); const scratch = NodeFS.mkdtempSync(NodePath.join(scratchRoot, "acp-pthread-spawn-")); diff --git a/apps/server/src/provider/antigravityAuthSupport.test.ts b/apps/server/src/provider/antigravityAuthSupport.test.ts index 8f794b59a1af..a1be101b1436 100644 --- a/apps/server/src/provider/antigravityAuthSupport.test.ts +++ b/apps/server/src/provider/antigravityAuthSupport.test.ts @@ -5,11 +5,7 @@ import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import * as NodePath from "@effect/platform-node/NodePath"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { ProviderInstanceId } from "@t3tools/contracts"; -import { - HostProcessExecutablePath, - HostProcessIsExecutable, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -574,8 +570,8 @@ it.layer(NodeServices.layer)("Antigravity profile preparation", (it) => { expect(profile.browserCommand).not.toContain("/packaged/t3"); expect(yield* fs.exists(profile.acpDirectory)).toBe(true); }).pipe( - Effect.provideService(HostProcessIsExecutable, true), - Effect.provideService(HostProcessExecutablePath, "/packaged/t3"), + Effect.provideService(HostProcess.IsExecutable, true), + Effect.provideService(HostProcess.ExecutablePath, "/packaged/t3"), ), ); @@ -601,7 +597,7 @@ it.layer(NodeServices.layer)("Antigravity profile preparation", (it) => { }); } expect(yield* fs.exists(profileDirectory)).toBe(false); - }).pipe(Effect.provideService(HostProcessIsExecutable, true)), + }).pipe(Effect.provideService(HostProcess.IsExecutable, true)), ); it.effect("preflights the no-browser helper and creates private directories only", () => @@ -616,7 +612,7 @@ it.layer(NodeServices.layer)("Antigravity profile preparation", (it) => { expect(profile.geminiHome).toBe(path.join(temporaryDirectory, "profile")); expect(yield* fs.exists(profile.acpDirectory)).toBe(true); expect(yield* fs.exists(profile.tokenPath)).toBe(false); - if ((yield* HostProcessPlatform) !== "win32") { + if ((yield* HostProcess.Platform) !== "win32") { expect((yield* fs.stat(profile.geminiHome)).mode & 0o777).toBe(0o700); expect((yield* fs.stat(profile.acpDirectory)).mode & 0o777).toBe(0o700); } diff --git a/apps/server/src/provider/antigravityAuthSupport.ts b/apps/server/src/provider/antigravityAuthSupport.ts index 4b0e769cde1d..a78ba04aa360 100644 --- a/apps/server/src/provider/antigravityAuthSupport.ts +++ b/apps/server/src/provider/antigravityAuthSupport.ts @@ -2,7 +2,7 @@ import * as NodeFSP from "node:fs/promises"; import type { AntigravityAuthMethod, ProviderInstanceId } from "@t3tools/contracts"; -import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveNodeExecutable, nodeRuntimeUnavailableMessage } from "@t3tools/shared/nodeRuntime"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; @@ -315,13 +315,13 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")( const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const platform = input.platform ?? (yield* HostProcessPlatform); + const platform = input.platform ?? (yield* HostProcess.Platform); const userHome = input.userHome ?? resolveAntigravityUserHome( platform, input.baseEnv ?? process.env, - yield* HostProcessHomeDirectory, + yield* HostProcess.HomeDirectory, ); const runtimeExecutablePath = input.runtimeExecutablePath ?? diff --git a/apps/server/src/provider/claudeResetCredits.test.ts b/apps/server/src/provider/claudeResetCredits.test.ts index 0dcb409ffa3c..a928e64c9fd5 100644 --- a/apps/server/src/provider/claudeResetCredits.test.ts +++ b/apps/server/src/provider/claudeResetCredits.test.ts @@ -1,6 +1,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { it as effectIt } from "@effect/vitest"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -111,7 +111,7 @@ effectIt.layer(NodeServices.layer)("readClaudeResetCredits", (it) => { ); }); const credits = yield* ClaudeResetCredits.readClaudeResetCredits(configDir, "2.1.0").pipe( - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), Effect.provideService(HttpClient.HttpClient, client), ); expect(credits).toEqual({ availableCount: 1, nextCreditId: "grant_a" }); @@ -122,11 +122,11 @@ effectIt.layer(NodeServices.layer)("readClaudeResetCredits", (it) => { Effect.gen(function* () { const { configDir } = yield* writeLogin; const darwin = yield* ClaudeResetCredits.readClaudeResetCredits(configDir, "2.1.0").pipe( - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Platform, "darwin"), Effect.provideService(HttpClient.HttpClient, refuseRequests), ); const limited = yield* ClaudeResetCredits.readClaudeResetCredits(configDir, "2.1.0").pipe( - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), Effect.provideService(HttpClient.HttpClient, respond(429, {})), ); expect([darwin, limited]).toEqual([undefined, undefined]); @@ -147,7 +147,7 @@ const consume = (client: HttpClient.HttpClient, ids = { grantId: "grant_a", requ version: "2.1.0", ...ids, }).pipe( - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), Effect.provideService(HttpClient.HttpClient, client), Effect.result, ); @@ -228,7 +228,7 @@ effectIt.layer(NodeServices.layer)("consumeClaudeResetCredit", (it) => { grantId: "grant_a", requestId: "r-1", }).pipe( - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), Effect.provideService(HttpClient.HttpClient, client), Effect.result, Effect.forkChild, diff --git a/apps/server/src/provider/claudeResetCredits.ts b/apps/server/src/provider/claudeResetCredits.ts index c37a0dfb2d8d..e381fa7cabe2 100644 --- a/apps/server/src/provider/claudeResetCredits.ts +++ b/apps/server/src/provider/claudeResetCredits.ts @@ -11,7 +11,7 @@ import type { ProviderConsumeResetCreditOutcome, ServerProviderResetCredits, } from "@t3tools/contracts"; -import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -149,7 +149,7 @@ const readJson = (schema: S, file: string) => const readAccessToken = (configDir: string) => Effect.gen(function* () { - if ((yield* HostProcessPlatform) === "darwin") return undefined; + if ((yield* HostProcess.Platform) === "darwin") return undefined; const path = yield* Path.Path; const credentials = yield* readJson(Credentials, path.join(configDir, ".credentials.json")); return credentials.claudeAiOauth?.accessToken?.trim() || undefined; @@ -195,7 +195,7 @@ export const claudeAccountConfigPath = Effect.fn("claudeAccountConfigPath")(func const path = yield* Path.Path; return configDir ? path.join(configDir, ".claude.json") - : path.join(yield* HostProcessHomeDirectory, ".claude.json"); + : path.join(yield* HostProcess.HomeDirectory, ".claude.json"); }); const CLAIM_OUTCOMES = { diff --git a/apps/server/src/provider/providerInstallation.test.ts b/apps/server/src/provider/providerInstallation.test.ts index ae2064fc85fa..f71f7c1ecfd8 100644 --- a/apps/server/src/provider/providerInstallation.test.ts +++ b/apps/server/src/provider/providerInstallation.test.ts @@ -5,7 +5,7 @@ import { ProviderInstanceId, type ProviderInstallState, } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; @@ -215,7 +215,7 @@ describe("provider installation routing", () => { Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-provider-install-route-" }); const binary = platform === "win32" ? "agy-test.exe" : "agy-test"; const executable = path.join(directory, binary); diff --git a/apps/server/src/provider/providerMaintenanceRunner.test.ts b/apps/server/src/provider/providerMaintenanceRunner.test.ts index 3b820093542c..26b33a35f4a2 100644 --- a/apps/server/src/provider/providerMaintenanceRunner.test.ts +++ b/apps/server/src/provider/providerMaintenanceRunner.test.ts @@ -20,7 +20,7 @@ import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; import { HttpClient, HttpClientResponse } from "effect/http"; import { ChildProcessSpawner } from "effect/process"; -import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { SpawnExecutableResolution } from "@t3tools/shared/shell"; import * as ProviderRegistry from "./ProviderRegistry.ts"; @@ -45,7 +45,7 @@ const encoder = new TextEncoder(); // `{ command, args }` assertions below hold deterministically on any host // (including Windows). Windows-specific resolution is covered by the dedicated // win32 case at the end of this suite. -const layerNonWindowsPlatform = Layer.succeed(HostProcessPlatform, "linux"); +const layerNonWindowsPlatform = Layer.succeed(HostProcess.Platform, "linux"); function lifecycleFor(provider: ProviderDriverKind): ProviderMaintenanceCapabilities { if (provider === NATIVE_CLI_DRIVER) { @@ -962,8 +962,8 @@ describe("providerMaintenanceRunner", () => { }).pipe( Effect.provide( Layer.mergeAll( - Layer.succeed(HostProcessPlatform, "win32"), - Layer.succeed(HostProcessEnvironment, { + Layer.succeed(HostProcess.Platform, "win32"), + Layer.succeed(HostProcess.Environment, { PATH: "C:\\fake\\npm", PATHEXT: ".COM;.EXE;.BAT;.CMD", }), diff --git a/apps/server/src/resourceTelemetry/HostResources.ts b/apps/server/src/resourceTelemetry/HostResources.ts index 607172b0f109..c3bb1a1894bc 100644 --- a/apps/server/src/resourceTelemetry/HostResources.ts +++ b/apps/server/src/resourceTelemetry/HostResources.ts @@ -1,6 +1,6 @@ import * as NodeOS from "node:os"; import type { HostResourcesSnapshot } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Cache from "effect/Cache"; import * as Context from "effect/Context"; import * as DateTime from "effect/DateTime"; @@ -40,7 +40,7 @@ function darwinAvailableMemory(output: string): number | null { const make = Effect.fn("makeHostResources")(function* () { const fs = yield* FileSystem.FileSystem; - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const sample = Effect.fn("HostResources.sample")(function* () { diff --git a/apps/server/src/resourceTelemetry/ResourceMonitorBinary.test.ts b/apps/server/src/resourceTelemetry/ResourceMonitorBinary.test.ts index 7032691f0dd0..d53e1a091f47 100644 --- a/apps/server/src/resourceTelemetry/ResourceMonitorBinary.test.ts +++ b/apps/server/src/resourceTelemetry/ResourceMonitorBinary.test.ts @@ -1,9 +1,5 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; -import { - HostProcessArchitecture, - HostProcessEnvironment, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { afterEach, assert, describe, expect, it, vi } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -13,7 +9,7 @@ import * as ResourceMonitorBinary from "./ResourceMonitorBinary.ts"; // The override checks POSIX exec bits on a real file under a linux platform // mock; NTFS never reports those bits, so the check cannot be satisfied there. -const windowsHost = HostProcessPlatform.defaultValue() === "win32"; +const windowsHost = HostProcess.Platform.defaultValue() === "win32"; describe("ResourceMonitorBinary", () => { afterEach(() => { @@ -34,9 +30,9 @@ describe("ResourceMonitorBinary", () => { const service = yield* ResourceMonitorBinary.make().pipe( Effect.provide(ServerConfig.layerTest(process.cwd(), baseDir)), - Effect.provideService(HostProcessPlatform, "win32"), - Effect.provideService(HostProcessArchitecture, "arm64"), - Effect.provideService(HostProcessEnvironment, { + Effect.provideService(HostProcess.Platform, "win32"), + Effect.provideService(HostProcess.Architecture, "arm64"), + Effect.provideService(HostProcess.Environment, { T3CODE_RESOURCE_MONITOR_PATH: binaryPath, }), ); @@ -58,10 +54,10 @@ describe("ResourceMonitorBinary", () => { const service = yield* ResourceMonitorBinary.make().pipe( Effect.provide(ServerConfig.layerTest(process.cwd(), baseDir)), - Effect.provideService(HostProcessPlatform, "linux"), - Effect.provideService(HostProcessArchitecture, "x64"), + Effect.provideService(HostProcess.Platform, "linux"), + Effect.provideService(HostProcess.Architecture, "x64"), Effect.provideService(ResourceMonitorBinary.ResourceMonitorHostLinuxLibc, "musl"), - Effect.provideService(HostProcessEnvironment, { + Effect.provideService(HostProcess.Environment, { T3CODE_RESOURCE_MONITOR_PATH: binaryPath, }), ); @@ -82,9 +78,9 @@ describe("ResourceMonitorBinary", () => { const service = yield* ResourceMonitorBinary.make().pipe( Effect.provide(ServerConfig.layerTest(process.cwd(), baseDir)), - Effect.provideService(HostProcessPlatform, "freebsd"), - Effect.provideService(HostProcessArchitecture, "ia32"), - Effect.provideService(HostProcessEnvironment, { + Effect.provideService(HostProcess.Platform, "freebsd"), + Effect.provideService(HostProcess.Architecture, "ia32"), + Effect.provideService(HostProcess.Environment, { T3CODE_RESOURCE_MONITOR_PATH: binaryPath, }), ); @@ -105,10 +101,10 @@ describe("ResourceMonitorBinary", () => { const service = yield* ResourceMonitorBinary.make().pipe( Effect.provide(ServerConfig.layerTest(process.cwd(), baseDir)), - Effect.provideService(HostProcessPlatform, "linux"), - Effect.provideService(HostProcessArchitecture, "x64"), + Effect.provideService(HostProcess.Platform, "linux"), + Effect.provideService(HostProcess.Architecture, "x64"), Effect.provideService(ResourceMonitorBinary.ResourceMonitorHostLinuxLibc, "gnu"), - Effect.provideService(HostProcessEnvironment, { + Effect.provideService(HostProcess.Environment, { T3CODE_RESOURCE_MONITOR_PATH: binaryPath, }), ); @@ -127,9 +123,9 @@ describe("ResourceMonitorBinary", () => { }); const service = yield* ResourceMonitorBinary.make().pipe( Effect.provide(ServerConfig.layerTest(process.cwd(), baseDir)), - Effect.provideService(HostProcessPlatform, "freebsd"), - Effect.provideService(HostProcessArchitecture, "ia32"), - Effect.provideService(HostProcessEnvironment, {}), + Effect.provideService(HostProcess.Platform, "freebsd"), + Effect.provideService(HostProcess.Architecture, "ia32"), + Effect.provideService(HostProcess.Environment, {}), ); const error = yield* Effect.flip(service.resolve); @@ -145,10 +141,10 @@ describe("ResourceMonitorBinary", () => { }); const service = yield* ResourceMonitorBinary.make().pipe( Effect.provide(ServerConfig.layerTest(process.cwd(), baseDir)), - Effect.provideService(HostProcessPlatform, "linux"), - Effect.provideService(HostProcessArchitecture, "x64"), + Effect.provideService(HostProcess.Platform, "linux"), + Effect.provideService(HostProcess.Architecture, "x64"), Effect.provideService(ResourceMonitorBinary.ResourceMonitorHostLinuxLibc, "musl"), - Effect.provideService(HostProcessEnvironment, {}), + Effect.provideService(HostProcess.Environment, {}), ); const error = yield* Effect.flip(service.resolve); diff --git a/apps/server/src/resourceTelemetry/ResourceMonitorBinary.ts b/apps/server/src/resourceTelemetry/ResourceMonitorBinary.ts index c4b9d6a85149..035a9364a497 100644 --- a/apps/server/src/resourceTelemetry/ResourceMonitorBinary.ts +++ b/apps/server/src/resourceTelemetry/ResourceMonitorBinary.ts @@ -1,8 +1,4 @@ -import { - HostProcessArchitecture, - HostProcessEnvironment, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -139,9 +135,9 @@ export const make = Effect.fn("resourceTelemetry.resourceMonitorBinary.make")(fu const config = yield* ServerConfig.ServerConfig; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const platform = yield* HostProcessPlatform; - const architecture = yield* HostProcessArchitecture; - const environment = yield* HostProcessEnvironment; + const platform = yield* HostProcess.Platform; + const architecture = yield* HostProcess.Architecture; + const environment = yield* HostProcess.Environment; const linuxLibc = platform === "linux" ? yield* ResourceMonitorHostLinuxLibc : undefined; const executableName = binaryName(platform); const platformKey = resourceMonitorPlatformKey(platform, architecture); diff --git a/apps/server/src/review/ReviewService.ts b/apps/server/src/review/ReviewService.ts index ef7a0e31590f..d8e27c9d6dd7 100644 --- a/apps/server/src/review/ReviewService.ts +++ b/apps/server/src/review/ReviewService.ts @@ -20,7 +20,7 @@ import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; import * as ServerSettings from "../serverSettings.ts"; import { isFilesystemRoot, managedWorktreesDirectories } from "../worktreesDirectory.ts"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; export class ReviewService extends Context.Service< ReviewService, @@ -74,7 +74,7 @@ export const make = Effect.gen(function* () { const worktreesDirectories = yield* settings.getSettings.pipe( Effect.orElseSucceed(() => ({ worktreesDirectory: "", previousWorktreesDirectories: [] })), ); - const home = yield* HostProcessHomeDirectory; + const home = yield* HostProcess.HomeDirectory; const [candidate, workspaceRoot, worktreesRoots] = yield* Effect.all([ canonicalizePath(cwd), canonicalizePath(config.cwd), diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.ts index e595b4028cee..d17f860050bb 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.ts @@ -29,7 +29,7 @@ import { import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError); const isSourceControlProviderError = Schema.is(SourceControlProviderError); const isBitbucketRepositoryLocatorError = Schema.is(BitbucketApi.BitbucketRepositoryLocatorError); @@ -214,7 +214,7 @@ export const make = Effect.gen(function* () { }); } - return path.resolve(expandHomePath(trimmed, yield* HostProcessHomeDirectory)); + return path.resolve(expandHomePath(trimmed, yield* HostProcess.HomeDirectory)); }, ); diff --git a/apps/server/src/storageCleanup.ts b/apps/server/src/storageCleanup.ts index c0ad95a63c61..7804ed42ef10 100644 --- a/apps/server/src/storageCleanup.ts +++ b/apps/server/src/storageCleanup.ts @@ -38,7 +38,7 @@ import * as TerminalManager from "./terminal/Manager.ts"; import { isFilesystemRoot, managedWorktreesDirectories } from "./worktreesDirectory.ts"; import * as GitVcsDriver from "./vcs/GitVcsDriver.ts"; import { withWorkspaceLease } from "./workspace/workspaceLease.ts"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; const decodeCleanupThread = Schema.decodeUnknownEffect( Schema.fromJsonString(OrchestrationV2AppThreadJson), @@ -214,7 +214,7 @@ export const make = Effect.gen(function* () { serverSettings, config.worktreesDir, path, - yield* HostProcessHomeDirectory, + yield* HostProcess.HomeDirectory, )) { // An unmounted drive only skips its own worktrees. const root = yield* fs.exists(directory).pipe( diff --git a/apps/server/src/telemetry/AnalyticsService.test.ts b/apps/server/src/telemetry/AnalyticsService.test.ts index e0a4786ece6f..73f844e4017c 100644 --- a/apps/server/src/telemetry/AnalyticsService.test.ts +++ b/apps/server/src/telemetry/AnalyticsService.test.ts @@ -11,7 +11,7 @@ import * as HttpClientError from "effect/http/HttpClientError"; import * as HttpServer from "effect/http/HttpServer"; import * as HttpServerRequest from "effect/http/HttpServerRequest"; import * as HttpServerResponse from "effect/http/HttpServerResponse"; -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as ServerConfig from "../config.ts"; import { getTelemetryIdentifier } from "./Identify.ts"; @@ -105,8 +105,8 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => { ), Layer.provide( Layer.mergeAll( - Layer.succeed(HostProcessPlatform, "win32"), - Layer.succeed(HostProcessArchitecture, "x64"), + Layer.succeed(HostProcess.Platform, "win32"), + Layer.succeed(HostProcess.Architecture, "x64"), layerAcceptThenFailClient(batches), ), ), @@ -167,8 +167,8 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => { Layer.provide(layerConfig), Layer.provide( Layer.mergeAll( - Layer.succeed(HostProcessPlatform, "linux"), - Layer.succeed(HostProcessArchitecture, "arm64"), + Layer.succeed(HostProcess.Platform, "linux"), + Layer.succeed(HostProcess.Architecture, "arm64"), ), ), Layer.provideMerge(NodeHttpServer.layerTest), @@ -257,8 +257,8 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => { Layer.provide(layerConfig), Layer.provide( Layer.mergeAll( - Layer.succeed(HostProcessPlatform, "linux"), - Layer.succeed(HostProcessArchitecture, "arm64"), + Layer.succeed(HostProcess.Platform, "linux"), + Layer.succeed(HostProcess.Architecture, "arm64"), ), ), Layer.provideMerge(NodeHttpServer.layerTest), diff --git a/apps/server/src/telemetry/AnalyticsService.ts b/apps/server/src/telemetry/AnalyticsService.ts index 3d10e93a26d6..864979759588 100644 --- a/apps/server/src/telemetry/AnalyticsService.ts +++ b/apps/server/src/telemetry/AnalyticsService.ts @@ -8,7 +8,7 @@ * * @module AnalyticsService */ -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import type { ClientOs } from "@t3tools/contracts"; import * as Clock from "effect/Clock"; import * as Config from "effect/Config"; @@ -136,8 +136,8 @@ export const make = Effect.gen(function* () { // The background flush and the shutdown flush must not send the same batch at once. const flushLock = yield* Semaphore.make(1); const clientType = serverConfig.mode === "desktop" ? "desktop-app" : "cli-web-client"; - const hostPlatform = yield* HostProcessPlatform; - const hostArchitecture = yield* HostProcessArchitecture; + const hostPlatform = yield* HostProcess.Platform; + const hostArchitecture = yield* HostProcess.Architecture; const enqueueBufferedEvent = ( uuid: string, diff --git a/apps/server/src/telemetry/Identify.ts b/apps/server/src/telemetry/Identify.ts index a0e6c1c7ecfd..6704300f87a4 100644 --- a/apps/server/src/telemetry/Identify.ts +++ b/apps/server/src/telemetry/Identify.ts @@ -9,7 +9,7 @@ import * as Schema from "effect/Schema"; import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite"; import * as ServerConfig from "../config.ts"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; /** * Codex writes `tokens` only for ChatGPT logins and omits the key for API-key, @@ -312,6 +312,6 @@ export const getTelemetryIdentifierForHome = Effect.fn("getTelemetryIdentifierFo ); export const getTelemetryIdentifier = Effect.flatMap( - HostProcessHomeDirectory, + HostProcess.HomeDirectory, getTelemetryIdentifierForHome, ); diff --git a/apps/server/src/terminal/Manager.test.ts b/apps/server/src/terminal/Manager.test.ts index 5a40f7bbb933..9889f33d4a8f 100644 --- a/apps/server/src/terminal/Manager.test.ts +++ b/apps/server/src/terminal/Manager.test.ts @@ -12,7 +12,7 @@ import { ServerSettingsError, TerminalProviderInstanceNotFoundError, } from "@t3tools/contracts"; -import { HostProcessPlatform, HostProcessArchitecture } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Data from "effect/Data"; import * as Clock from "effect/Clock"; import * as Deferred from "effect/Deferred"; @@ -315,7 +315,7 @@ const createManager = ( ); const layerWithHostPlatform = (platform: NodeJS.Platform) => - Layer.succeed(HostProcessPlatform, platform); + Layer.succeed(HostProcess.Platform, platform); // Apply the existing line policy, then find the longest code-point-aligned byte tail. function retainedHistory(text: string, maxLines: number, maxBytes = Infinity): string { @@ -619,7 +619,7 @@ it.layer( it.effect("preserves non-notFound cwd stat failures", () => Effect.gen(function* () { - if ((yield* HostProcessPlatform) === "win32") return; + if ((yield* HostProcess.Platform) === "win32") return; const path = yield* Path.Path; @@ -1826,7 +1826,7 @@ it.layer( it.effect("retries with fallback shells when preferred shell spawn fails", () => Effect.gen(function* () { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const missingShell = platform === "win32" ? "C:\\definitely\\missing-shell.exe" : "/definitely/missing-shell -l"; const { manager, ptyAdapter } = yield* createManager(5, { @@ -1933,7 +1933,7 @@ it.layer( Effect.provide( Layer.merge( layerWithHostPlatform("win32"), - Layer.succeed(HostProcessArchitecture, "x64"), + Layer.succeed(HostProcess.Architecture, "x64"), ), ), ); @@ -2513,7 +2513,7 @@ it.layer( it.effect("starts zsh with prompt spacer disabled to avoid `%` end markers", () => Effect.gen(function* () { - if ((yield* HostProcessPlatform) === "win32") return; + if ((yield* HostProcess.Platform) === "win32") return; const { manager, ptyAdapter } = yield* createManager(5, { shellResolver: () => "/bin/zsh", }); diff --git a/apps/server/src/terminal/Manager.ts b/apps/server/src/terminal/Manager.ts index 2a16b9d44f75..d8b2cf3e0553 100644 --- a/apps/server/src/terminal/Manager.ts +++ b/apps/server/src/terminal/Manager.ts @@ -42,11 +42,7 @@ import { } from "@t3tools/contracts"; import { makeKeyedCoalescingWorker } from "@t3tools/shared/KeyedCoalescingWorker"; import * as KeyedLock from "@t3tools/shared/KeyedLock"; -import { - HostProcessArchitecture, - HostProcessHomeDirectory, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { mergePathEntries } from "@t3tools/shared/shell"; import { acpRegistryManagedBinaryDirectories } from "@t3tools/provider-acp-registry/server"; @@ -1482,8 +1478,8 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func const logsDir = options.logsDir; const historyLineLimit = options.historyLineLimit ?? DEFAULT_HISTORY_LINE_LIMIT; const historyByteLimit = options.historyByteLimit ?? DEFAULT_HISTORY_BYTE_LIMIT; - const platform = yield* HostProcessPlatform; - const architecture = yield* HostProcessArchitecture; + const platform = yield* HostProcess.Platform; + const architecture = yield* HostProcess.Architecture; // Terminals must inherit the user's full environment (minus the blocklist // applied in createTerminalSpawnEnv) — an allowlist here silently strips // things like PSModulePath, DISPLAY, proxies, and toolchain variables. @@ -2253,7 +2249,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func baseEnv, session.runtimeEnv, platform, - yield* HostProcessHomeDirectory, + yield* HostProcess.HomeDirectory, ); // Append (never prepend) managed ACP agent install directories so // `kimi login` and friends resolve by name without shadowing any diff --git a/apps/server/src/terminal/NodePtyAdapter.test.ts b/apps/server/src/terminal/NodePtyAdapter.test.ts index e8c2e4d75ff7..79f73c2e9060 100644 --- a/apps/server/src/terminal/NodePtyAdapter.test.ts +++ b/apps/server/src/terminal/NodePtyAdapter.test.ts @@ -3,7 +3,7 @@ import * as NodeNet from "node:net"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; @@ -66,8 +66,8 @@ const layerTestFor = (platform: NodeJS.Platform = "win32") => Layer.provide( Layer.mergeAll( NodeServices.layer, - Layer.succeed(HostProcessPlatform, platform), - Layer.succeed(HostProcessArchitecture, "x64"), + Layer.succeed(HostProcess.Platform, platform), + Layer.succeed(HostProcess.Architecture, "x64"), Layer.succeed(NodePtyAdapter.NodePtyModuleLoaderRef, () => Promise.resolve(fakeNodePty)), ), ), @@ -272,8 +272,8 @@ it.effect("reports native module load failures as structured startup defects", ( Effect.provide( Layer.mergeAll( NodeServices.layer, - Layer.succeed(HostProcessPlatform, "win32"), - Layer.succeed(HostProcessArchitecture, "x64"), + Layer.succeed(HostProcess.Platform, "win32"), + Layer.succeed(HostProcess.Architecture, "x64"), ), ), ), diff --git a/apps/server/src/terminal/NodePtyAdapter.ts b/apps/server/src/terminal/NodePtyAdapter.ts index ed4aee84006e..e865c3020a80 100644 --- a/apps/server/src/terminal/NodePtyAdapter.ts +++ b/apps/server/src/terminal/NodePtyAdapter.ts @@ -7,7 +7,7 @@ import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as PtyAdapter from "@t3tools/shared/PtyAdapter"; @@ -46,8 +46,8 @@ let didEnsureSpawnHelperExecutable = false; const resolveNodePtySpawnHelperPath = Effect.gen(function* () { const path = yield* Path.Path; const fs = yield* FileSystem.FileSystem; - const platform = yield* HostProcessPlatform; - const architecture = yield* HostProcessArchitecture; + const platform = yield* HostProcess.Platform; + const architecture = yield* HostProcess.Architecture; const packageJsonPath = requireForNodePty.resolve("node-pty/package.json"); const packageDir = path.dirname(packageJsonPath); @@ -67,7 +67,7 @@ const resolveNodePtySpawnHelperPath = Effect.gen(function* () { const ensureNodePtySpawnHelperExecutable = Effect.fn(function* () { const fs = yield* FileSystem.FileSystem; - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; if (platform === "win32") return; if (didEnsureSpawnHelperExecutable) return; @@ -236,8 +236,8 @@ export const make = Effect.fn("NodePtyAdapter.make")(function* () { const loadNodePtyModule = yield* NodePtyModuleLoaderRef; const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const platform = yield* HostProcessPlatform; - const architecture = yield* HostProcessArchitecture; + const platform = yield* HostProcess.Platform; + const architecture = yield* HostProcess.Architecture; const nodePty = yield* Effect.tryPromise({ try: loadNodePtyModule, @@ -253,8 +253,8 @@ export const make = Effect.fn("NodePtyAdapter.make")(function* () { ensureNodePtySpawnHelperExecutable().pipe( Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path), - Effect.provideService(HostProcessPlatform, platform), - Effect.provideService(HostProcessArchitecture, architecture), + Effect.provideService(HostProcess.Platform, platform), + Effect.provideService(HostProcess.Architecture, architecture), Effect.orElseSucceed(() => undefined), ), ); diff --git a/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts b/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts index 29c0f950f7c8..577c122f65e0 100644 --- a/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts +++ b/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts @@ -1,7 +1,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { it } from "@effect/vitest"; import { ClaudeSettings, ProviderInstanceId } from "@t3tools/contracts"; -import { HostProcessPlatform, isHostWindows } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { createModelSelection } from "@t3tools/shared/model"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -35,7 +35,7 @@ function makeFakeClaudeBinary(dir: string) { return Effect.gen(function* () { const path = yield* Path.Path; const fs = yield* FileSystem.FileSystem; - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const binDir = path.join(dir, "bin"); const fixturePath = yield* path.fromFileUrl( new URL("./testing/ClaudeTextGeneration.fixture.mjs", import.meta.url), @@ -69,7 +69,7 @@ function withFakeClaudeEnv( const fs = yield* FileSystem.FileSystem; const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-claude-text-" }); const binDir = yield* makeFakeClaudeBinary(tempDir); - const pathDelimiter = (yield* isHostWindows) ? ";" : ":"; + const pathDelimiter = (yield* HostProcess.isWindows) ? ";" : ":"; const previousPath = process.env.PATH; const previousOutput = process.env.T3_FAKE_CLAUDE_OUTPUT; const previousExitCode = process.env.T3_FAKE_CLAUDE_EXIT_CODE; diff --git a/apps/server/src/textGeneration/CodexTextGeneration.ts b/apps/server/src/textGeneration/CodexTextGeneration.ts index c11fed75814d..3902b0c74d78 100644 --- a/apps/server/src/textGeneration/CodexTextGeneration.ts +++ b/apps/server/src/textGeneration/CodexTextGeneration.ts @@ -26,7 +26,7 @@ import { } from "@t3tools/provider-core/server/textGenerationUtils"; import { codexModelFamily, getModelSelectionStringOptionValue } from "@t3tools/shared/model"; import { getCodexServiceTierOptionValue } from "../codexModelOptions.ts"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; const CODEX_TIMEOUT_MS = 180_000; const encodeJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); @@ -218,7 +218,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func ? { CODEX_HOME: expandHomePath( effectiveConfig.homePath, - yield* HostProcessHomeDirectory, + yield* HostProcess.HomeDirectory, ), } : {}), diff --git a/apps/server/src/usage/UsageService.test.ts b/apps/server/src/usage/UsageService.test.ts index 9c1e62fe1a7d..cb9270a7ed3b 100644 --- a/apps/server/src/usage/UsageService.test.ts +++ b/apps/server/src/usage/UsageService.test.ts @@ -8,7 +8,7 @@ import * as NodeSqlite from "node:sqlite"; import { assert, describe, it } from "@effect/vitest"; import * as NodeServices from "@effect/platform-node/NodeServices"; -import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { mergeUsage } from "@t3tools/shared/usageMerge"; import { EnvironmentId, @@ -134,7 +134,7 @@ const layerService = (input: { Layer.provideMerge(Layer.mock(ServerSecretStore.ServerSecretStore)({})), Layer.provideMerge(ServerConfig.layerTest(process.cwd(), { prefix: input.prefix })), Layer.provideMerge(NodeServices.layer), - Layer.provideMerge(Layer.succeed(HostProcessPlatform, input.platform ?? "linux")), + Layer.provideMerge(Layer.succeed(HostProcess.Platform, input.platform ?? "linux")), Layer.provideMerge(ServerSettings.layerTest(input.settings)), Layer.provideMerge( Layer.succeed( @@ -150,7 +150,7 @@ const layerService = (input: { ), ), Layer.provideMerge( - Layer.succeed(HostProcessEnvironment, { + Layer.succeed(HostProcess.Environment, { HOME: input.home, GROK_HOME: NodePath.join(input.home, "grok"), OPENCODE_DATA_DIR: NodePath.join(input.home, "opencode"), @@ -1356,7 +1356,7 @@ describe("UsageService", () => { }).pipe(Effect.scoped), ); - it.live.skipIf(HostProcessPlatform.defaultValue() === "win32")( + it.live.skipIf(HostProcess.Platform.defaultValue() === "win32")( "keeps a newer cached read when a slower scan of another window finishes later", () => Effect.gen(function* () { @@ -1387,7 +1387,7 @@ describe("UsageService", () => { }).pipe(Effect.scoped), ); - it.live.skipIf(HostProcessPlatform.defaultValue() === "win32")( + it.live.skipIf(HostProcess.Platform.defaultValue() === "win32")( "keeps the later read when a scan that read earlier finishes first", () => Effect.gen(function* () { diff --git a/apps/server/src/usage/UsageService.ts b/apps/server/src/usage/UsageService.ts index 2ceeb8750001..3b496d8f4498 100644 --- a/apps/server/src/usage/UsageService.ts +++ b/apps/server/src/usage/UsageService.ts @@ -31,7 +31,7 @@ import { type UsageSummaryInput, UsageReadError, } from "@t3tools/contracts"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Cause from "effect/Cause"; import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; @@ -227,7 +227,7 @@ export const make = Effect.gen(function* () { const config = yield* ServerConfig.ServerConfig; const settingsService = yield* ServerSettings.ServerSettingsService; const httpClient = yield* HttpClient.HttpClient; - const hostEnvironment = yield* HostProcessEnvironment; + const hostEnvironment = yield* HostProcess.Environment; // The readers yield their own services; scans run them against this context. const readerContext = yield* Effect.context(); diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index a71226c06aee..fc0cdef01e5b 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -1,7 +1,7 @@ // @effect-diagnostics nodeBuiltinImport:off - realpathSync.native resolves Windows 8.3 short names, which the Effect realPath does not. import * as NodeFS from "node:fs"; import * as NodeServices from "@effect/platform-node/NodeServices"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { assert, it, describe } from "@effect/vitest"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; @@ -1864,7 +1864,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { yield* writeTextFile(cwd, " leading.txt", "whitespace path\n"); yield* writeTextFile(cwd, "l.txt", "other\n"); yield* writeTextFile(cwd, "binary.dat", "binary\0data"); - if ((yield* HostProcessPlatform) !== "win32") { + if ((yield* HostProcess.Platform) !== "win32") { yield* writeTextFile(cwd, "tab\tand\nnewline.txt", "unusual path\n"); } yield* git(cwd, ["add", "."]); @@ -1906,7 +1906,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { additions: 0, deletions: 0, }); - if ((yield* HostProcessPlatform) !== "win32") { + if ((yield* HostProcess.Platform) !== "win32") { assert.deepInclude(branch.files ?? [], { path: "tab\tand\nnewline.txt", previousPath: null, @@ -2890,7 +2890,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { }); // NTFS rejects a newline in a file name, so there is nothing to preserve there. - it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")( + it.effect.skipIf(HostProcess.Platform.defaultValue() === "win32")( "preserves newline characters in worktree paths when listing refs", () => Effect.gen(function* () { @@ -3965,7 +3965,7 @@ describe("Windows long path configuration", () => { const layer = GitVcsDriver.layer.pipe( Layer.provide(layerServerConfig), Layer.provideMerge(NodeServices.layer), - Layer.provide(Layer.succeed(HostProcessPlatform, platform)), + Layer.provide(Layer.succeed(HostProcess.Platform, platform)), ); return yield* Effect.gen(function* () { const driver = yield* GitVcsDriver.GitVcsDriver; diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index b273e9eff52f..51c1f7b29468 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -29,7 +29,7 @@ import { type VcsRef, } from "@t3tools/contracts"; import { dedupeRemoteBranchesWithLocalMatches, normalizeGitRemoteUrl } from "@t3tools/shared/git"; -import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { compactTraceAttributes } from "@t3tools/shared/observability"; import { decodeJsonResult } from "@t3tools/shared/schemaJson"; import { parseT3ProjectFile } from "@t3tools/shared/t3ProjectFile"; @@ -944,7 +944,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* const commandSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const { worktreesDir } = yield* ServerConfig.ServerConfig; const crypto = yield* Crypto.Crypto; - const hostPlatform = yield* HostProcessPlatform; + const hostPlatform = yield* HostProcess.Platform; const executeRaw: GitVcsDriver.GitVcsDriver["Service"]["execute"] = Effect.fnUntraced( function* (input) { @@ -2657,7 +2657,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* "hash-object", "-t", "tree", - (yield* HostProcessPlatform) === "win32" ? "NUL" : "/dev/null", + (yield* HostProcess.Platform) === "win32" ? "NUL" : "/dev/null", ]); return stdout.trim(); }); @@ -3403,7 +3403,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* options?.worktreesDirectory ?? "", worktreesDir, path, - yield* HostProcessHomeDirectory, + yield* HostProcess.HomeDirectory, ); if (parentDir === null) { return yield* new GitCommandError({ diff --git a/apps/server/src/vcs/VcsProcess.test.ts b/apps/server/src/vcs/VcsProcess.test.ts index 9962044fc905..bbe95780a1f4 100644 --- a/apps/server/src/vcs/VcsProcess.test.ts +++ b/apps/server/src/vcs/VcsProcess.test.ts @@ -1,6 +1,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, expect, it } from "@effect/vitest"; -import { HostProcessWorkingDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Duration from "effect/Duration"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; @@ -535,7 +535,7 @@ describe("VcsProcess.run", () => { operation: "test.output-limit", command: "node", args: ["-e", "process.stdout.write('x'.repeat(2048))"], - cwd: yield* HostProcessWorkingDirectory, + cwd: yield* HostProcess.WorkingDirectory, maxOutputBytes: 128, outputMode: "error", }).pipe(Effect.flip); diff --git a/apps/server/src/workspace/WorkspaceEntries.test.ts b/apps/server/src/workspace/WorkspaceEntries.test.ts index 31c806b8e78e..e827ae920985 100644 --- a/apps/server/src/workspace/WorkspaceEntries.test.ts +++ b/apps/server/src/workspace/WorkspaceEntries.test.ts @@ -11,7 +11,7 @@ import * as PlatformError from "effect/PlatformError"; import { vi } from "vite-plus/test"; import * as ServerConfig from "../config.ts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as VcsProcess from "../vcs/VcsProcess.ts"; import * as WorkspaceEntries from "./WorkspaceEntries.ts"; import * as WorkspacePaths from "./WorkspacePaths.ts"; @@ -84,7 +84,7 @@ const searchWorkspaceEntries = (input: { }); const appendSeparator = (input: string) => - Effect.map(HostProcessPlatform, (platform) => + Effect.map(HostProcess.Platform, (platform) => input.endsWith("/") || input.endsWith("\\") ? input : `${input}${platform === "win32" ? "\\" : "/"}`, @@ -143,7 +143,7 @@ it.layer(layerTest, { excludeTestServices: true })("WorkspaceEntries", (it) => { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; yield* writeTextFile(cwd, ".git/HEAD"); - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; if (platform !== "win32") yield* fileSystem.symlink(outside, path.join(cwd, "external")); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; for (const directoryPath of [ diff --git a/apps/server/src/workspace/WorkspaceEntries.ts b/apps/server/src/workspace/WorkspaceEntries.ts index 153360443338..8f312223dae1 100644 --- a/apps/server/src/workspace/WorkspaceEntries.ts +++ b/apps/server/src/workspace/WorkspaceEntries.ts @@ -19,7 +19,7 @@ import type { ProjectSearchEntriesInput, ProjectSearchEntriesResult, } from "@t3tools/contracts"; -import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { isExplicitRelativePath, isWindowsAbsolutePath } from "@t3tools/shared/path"; import { normalizeSearchQuery } from "@t3tools/shared/searchRanking"; @@ -110,7 +110,7 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu input: FilesystemBrowseInput, path: Path.Path, ): Effect.fn.Return { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; if (platform !== "win32" && isWindowsAbsolutePath(input.partialPath)) { return yield* new WorkspaceEntriesWindowsPathUnsupportedError({ cwd: input.cwd, @@ -120,7 +120,7 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu } if (!isExplicitRelativePath(input.partialPath)) { - return path.resolve(expandHomePath(input.partialPath, yield* HostProcessHomeDirectory)); + return path.resolve(expandHomePath(input.partialPath, yield* HostProcess.HomeDirectory)); } if (!input.cwd) { @@ -129,7 +129,7 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu }); } return path.resolve( - expandHomePath(input.cwd, yield* HostProcessHomeDirectory), + expandHomePath(input.cwd, yield* HostProcess.HomeDirectory), input.partialPath, ); }); diff --git a/apps/server/src/workspace/WorkspaceFileSystem.test.ts b/apps/server/src/workspace/WorkspaceFileSystem.test.ts index 9a22a27bd37c..885b0eabb4ad 100644 --- a/apps/server/src/workspace/WorkspaceFileSystem.test.ts +++ b/apps/server/src/workspace/WorkspaceFileSystem.test.ts @@ -14,7 +14,7 @@ import * as VcsProcess from "../vcs/VcsProcess.ts"; import * as WorkspaceEntries from "./WorkspaceEntries.ts"; import * as WorkspaceFileSystem from "./WorkspaceFileSystem.ts"; import * as WorkspacePaths from "./WorkspacePaths.ts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; const layerProject = WorkspaceFileSystem.layer.pipe( @@ -102,7 +102,7 @@ it.layer(layerTest, { excludeTestServices: true })("WorkspaceFileSystemLive", (i ); // Needs mkfifo; Windows has no FIFOs to reject. - it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")( + it.effect.skipIf(HostProcess.Platform.defaultValue() === "win32")( "rejects a FIFO without blocking on open", () => Effect.gen(function* () { diff --git a/apps/server/src/workspace/WorkspacePaths.ts b/apps/server/src/workspace/WorkspacePaths.ts index 748689330072..097c757a18e8 100644 --- a/apps/server/src/workspace/WorkspacePaths.ts +++ b/apps/server/src/workspace/WorkspacePaths.ts @@ -15,7 +15,7 @@ import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; export class WorkspaceRootNotExistsError extends Schema.TaggedError()( "WorkspaceRootNotExistsError", @@ -154,7 +154,7 @@ export const make = Effect.gen(function* () { "WorkspacePaths.normalizeWorkspaceRoot", )(function* (workspaceRoot, options) { const normalizedWorkspaceRoot = path.resolve( - expandHomePath(workspaceRoot.trim(), yield* HostProcessHomeDirectory), + expandHomePath(workspaceRoot.trim(), yield* HostProcess.HomeDirectory), ); let workspaceStat = yield* statWorkspaceRoot( workspaceRoot, diff --git a/apps/server/src/ws.test.ts b/apps/server/src/ws.test.ts index ae263ac15fd3..0f735efd31c7 100644 --- a/apps/server/src/ws.test.ts +++ b/apps/server/src/ws.test.ts @@ -4,7 +4,7 @@ import { type ServerConfig, type ServerConfigStreamEvent, } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as ConfigProvider from "effect/ConfigProvider"; import * as Deferred from "effect/Deferred"; import * as Duration from "effect/Duration"; @@ -164,7 +164,7 @@ const makeParkedWindowsLauncher = Effect.gen(function* () { ); const onWindows = (effect: Effect.Effect) => effect.pipe( - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), Effect.provide( ConfigProvider.layer( ConfigProvider.fromEnv({ diff --git a/oxlint-plugin-t3code/rules/no-global-process-runtime.test.ts b/oxlint-plugin-t3code/rules/no-global-process-runtime.test.ts index dc9cf6979a70..8a4a8816b25f 100644 --- a/oxlint-plugin-t3code/rules/no-global-process-runtime.test.ts +++ b/oxlint-plugin-t3code/rules/no-global-process-runtime.test.ts @@ -8,10 +8,10 @@ describe("t3code/no-global-process-runtime", () => { rule.valid( "allows injected host process references", ` - import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; + import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; - export const isWindows = Effect.map(HostProcessPlatform, (platform) => platform === "win32"); + export const isWindows = Effect.map(HostProcess.Platform, (platform) => platform === "win32"); `, ); @@ -38,7 +38,7 @@ describe("t3code/no-global-process-runtime", () => { export const isWindows = process.platform === "win32"; `, (output) => { - assert.match(output, /Use HostProcessPlatform/); + assert.match(output, /Use HostProcess\.Platform/); }, ); @@ -48,7 +48,7 @@ describe("t3code/no-global-process-runtime", () => { export const isArm = process.arch === "arm64"; `, (output) => { - assert.match(output, /Use HostProcessArchitecture/); + assert.match(output, /Use HostProcess\.Architecture/); }, ); @@ -67,7 +67,7 @@ describe("t3code/no-global-process-runtime", () => { export const isWindows = NodeOS.platform() === "win32"; `, (output) => { - assert.match(output, /Use HostProcessPlatform/); + assert.match(output, /Use HostProcess\.Platform/); }, ); @@ -79,7 +79,7 @@ describe("t3code/no-global-process-runtime", () => { export const isArm = hostArch() === "arm64"; `, (output) => { - assert.match(output, /Use HostProcessArchitecture/); + assert.match(output, /Use HostProcess\.Architecture/); }, ); diff --git a/oxlint-plugin-t3code/rules/no-global-process-runtime.ts b/oxlint-plugin-t3code/rules/no-global-process-runtime.ts index f122d16db29a..822094e922f2 100644 --- a/oxlint-plugin-t3code/rules/no-global-process-runtime.ts +++ b/oxlint-plugin-t3code/rules/no-global-process-runtime.ts @@ -19,7 +19,7 @@ const isGlobalProcessObject = (node: unknown): boolean => { }; const message = (property: string) => - `Use HostProcess${property === "arch" ? "Architecture" : "Platform"} instead of process.${property}; inject the runtime reference in Effect code and provide it explicitly in tests.`; + `Use HostProcess.${property === "arch" ? "Architecture" : "Platform"} instead of process.${property}; inject the runtime reference in Effect code and provide it explicitly in tests.`; const getLiteralStringValue = (node: unknown): Option.Option => { if (typeof node !== "object" || node === null) return Option.none(); diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts index 62fd6274e64d..f95286d004af 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts @@ -6,11 +6,7 @@ import { ProviderInstanceId, } from "@t3tools/contracts"; import { AcpRegistrySettings } from "../settings.ts"; -import { - HostProcessArchitecture, - HostProcessEnvironment, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { SpawnExecutableResolution } from "@t3tools/shared/shell"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; @@ -63,9 +59,9 @@ function layerResolver( return Layer.mergeAll( NodeServices.layer, TestProviderHost.layer().pipe(Layer.provide(NodeServices.layer)), - Layer.succeed(HostProcessPlatform, "linux"), - Layer.succeed(HostProcessArchitecture, "x64"), - Layer.succeed(HostProcessEnvironment, environment), + Layer.succeed(HostProcess.Platform, "linux"), + Layer.succeed(HostProcess.Architecture, "x64"), + Layer.succeed(HostProcess.Environment, environment), Layer.succeed(HttpClient.HttpClient, HttpClient.make(execute)), ); } @@ -229,7 +225,7 @@ describe("AcpRegistrySupport", () => { cacheDir, toolsDir: `${cacheDir}/tools`, registryUrl, - }).pipe(Effect.provideService(HostProcessEnvironment, hostEnvironment)); + }).pipe(Effect.provideService(HostProcess.Environment, hostEnvironment)); const localSettings = decodeAcpRegistrySettings({ source: "local", commandPath: "dsh", @@ -279,7 +275,7 @@ describe("AcpRegistrySupport", () => { cacheDir, toolsDir: `${cacheDir}/tools`, registryUrl, - }).pipe(Effect.provideService(HostProcessEnvironment, environment)); + }).pipe(Effect.provideService(HostProcess.Environment, environment)); const resolved = yield* resolver.resolve( decodeAcpRegistrySettings({ source: "local", commandPath }), "/workspace", @@ -300,7 +296,7 @@ describe("AcpRegistrySupport", () => { toolsDir: `${cacheDir}/tools`, registryUrl, }).pipe( - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), Effect.provideService(SpawnExecutableResolution, () => `C:\\bin\\dsh${extension}`), ); const localSettings = decodeAcpRegistrySettings({ source: "local", commandPath: "dsh" }); @@ -432,7 +428,7 @@ describe("AcpRegistrySupport", () => { cacheDir, toolsDir: `${cacheDir}/tools`, registryUrl, - }).pipe(Effect.provideService(HostProcessEnvironment, toolchain.environment)); + }).pipe(Effect.provideService(HostProcess.Environment, toolchain.environment)); const first = yield* resolver.resolve(settings(), "/workspace", toolchain.environment); const second = yield* resolver.resolve(settings(), "/workspace", toolchain.environment); @@ -478,7 +474,7 @@ describe("AcpRegistrySupport", () => { cacheDir, toolsDir: `${cacheDir}/tools`, registryUrl, - }).pipe(Effect.provideService(HostProcessEnvironment, environment)); + }).pipe(Effect.provideService(HostProcess.Environment, environment)); const resolved = yield* resolver.resolve(settings(), "/workspace", environment); const userGlobalBin = toolchain.globalBin; @@ -559,7 +555,7 @@ describe("AcpRegistrySupport", () => { toolsDir: `${cacheDir}/tools`, registryUrl, }).pipe( - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), Effect.provideService(SpawnExecutableResolution, (command) => { if (command === "uv") return toolchain.uvPath; if (command === "fast-agent") return toolchain.executablePath; @@ -677,7 +673,7 @@ describe("AcpRegistrySupport", () => { cacheDir, toolsDir: `${cacheDir}/tools`, registryUrl, - }).pipe(Effect.provideService(HostProcessEnvironment, environment)); + }).pipe(Effect.provideService(HostProcess.Environment, environment)); yield* resolver.search({ query: "example" }); expect(yield* resolver.inspect(settings(), environment)).toMatchObject({ status: "unprepared", @@ -781,8 +777,8 @@ describe("AcpRegistrySupport", () => { TestProviderHost.layer().pipe(Layer.provide(NodeServices.layer)), ), ), - Effect.provideService(HostProcessPlatform, "linux"), - Effect.provideService(HostProcessArchitecture, "x64"), + Effect.provideService(HostProcess.Platform, "linux"), + Effect.provideService(HostProcess.Architecture, "x64"), ); }); @@ -1097,7 +1093,7 @@ describe("AcpRegistrySupport", () => { cacheDir, toolsDir: `${cacheDir}/tools`, registryUrl, - }).pipe(Effect.provideService(HostProcessEnvironment, toolchain.environment)); + }).pipe(Effect.provideService(HostProcess.Environment, toolchain.environment)); const prepared = yield* resolver.prepare({ agentId: agent.id }); expect(prepared).toEqual({ agentId: "example-agent", diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts index fba8e656e615..aeb792b08ca8 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts @@ -12,11 +12,7 @@ import { } from "@t3tools/contracts"; import type { AcpRegistryDistributionPreference } from "../settings.ts"; import type { AcpRegistrySettings } from "../settings.ts"; -import { - HostProcessArchitecture, - HostProcessEnvironment, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { mergePathEntries, resolveSpawnCommand, @@ -637,9 +633,9 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct const httpClient = yield* HttpClient.HttpClient; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const { settings: hostSettings } = yield* ProviderHost.ProviderHost; - const platform = yield* HostProcessPlatform; - const architecture = yield* HostProcessArchitecture; - const hostEnvironment = yield* HostProcessEnvironment; + const platform = yield* HostProcess.Platform; + const architecture = yield* HostProcess.Architecture; + const hostEnvironment = yield* HostProcess.Environment; const resolveExecutable = yield* SpawnExecutableResolution; const platformTarget = resolveAcpRegistryPlatformTarget(platform, architecture); const registryUrl = input.registryUrl ?? ACP_REGISTRY_URL; diff --git a/packages/provider-acp-registry/src/server/adapter.ts b/packages/provider-acp-registry/src/server/adapter.ts index 4827ca4e4c56..d5fb3279f405 100644 --- a/packages/provider-acp-registry/src/server/adapter.ts +++ b/packages/provider-acp-registry/src/server/adapter.ts @@ -5,7 +5,7 @@ import { } from "./devinAcp.ts"; import { defaultInstanceIdForDriver, ProviderDriverKind } from "@t3tools/contracts"; import { AcpRegistrySettings } from "../settings.ts"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveSelfInvocation, type SelfInvocation } from "@t3tools/shared/nodeRuntime"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; @@ -271,7 +271,7 @@ export const AcpRegistryAdapterV2Driver: ProviderAdapterDriver< defaultConfig: (): AcpRegistrySettings => DEFAULT_ACP_REGISTRY_SETTINGS, create: Effect.fn("AcpRegistryAdapterV2Driver.create")( function* (input: ProviderAdapterDriverCreateInput) { - const hostEnvironment = yield* HostProcessEnvironment; + const hostEnvironment = yield* HostProcess.Environment; const selfInvocation = yield* resolveSelfInvocation(); const providerEventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers; const makeNativeLogger = yield* makeAcpNativeLoggerFactory(); diff --git a/packages/provider-acp-registry/src/server/driver.ts b/packages/provider-acp-registry/src/server/driver.ts index 4468aa70af39..8ddf7ad9ce23 100644 --- a/packages/provider-acp-registry/src/server/driver.ts +++ b/packages/provider-acp-registry/src/server/driver.ts @@ -8,7 +8,7 @@ import { type ServerProviderModel, } from "@t3tools/contracts"; import { AcpRegistrySettings } from "../settings.ts"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { createModelCapabilities } from "@t3tools/shared/model"; import * as Clock from "effect/Clock"; import * as Crypto from "effect/Crypto"; @@ -484,7 +484,7 @@ export const AcpRegistryDriver: ProviderDriver(); const stderrTailRef = yield* Ref.make(""); - const homeDirectory = yield* HostProcessHomeDirectory; + const homeDirectory = yield* HostProcess.HomeDirectory; const stderrDrained = yield* Deferred.make(); const runtimeClosed = yield* Deferred.make(); const promptSerializationSemaphore = yield* Semaphore.make(1); @@ -1729,7 +1729,7 @@ export const make = ( Effect.uninterruptible(terminateOwnedProcessGroupImpl), ); if (options.ownDetachedProcessGroup === true) { - const hostPlatform = yield* HostProcessPlatform; + const hostPlatform = yield* HostProcess.Platform; const forceTerminateOwnedProcessGroup = hostPlatform === "win32" ? terminateWindowsProcessTreeWithTaskkill(Number(child.pid)).pipe( diff --git a/packages/provider-core/src/server/instanceEnvironment.test.ts b/packages/provider-core/src/server/instanceEnvironment.test.ts index e64cd31650f5..1899d1f9d5c3 100644 --- a/packages/provider-core/src/server/instanceEnvironment.test.ts +++ b/packages/provider-core/src/server/instanceEnvironment.test.ts @@ -2,7 +2,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Path from "effect/Path"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { mergeProviderInstanceEnvironment } from "./instanceEnvironment.ts"; @@ -36,7 +36,7 @@ describe("mergeProviderInstanceEnvironment", () => { CLAUDE_CONFIG_DIR: "~/.inherited-claude", }); }).pipe( - Effect.provideService(HostProcessHomeDirectory, "/home/ada"), + Effect.provideService(HostProcess.HomeDirectory, "/home/ada"), Effect.provide(NodeServices.layer), ), ); diff --git a/packages/provider-core/src/server/instanceEnvironment.ts b/packages/provider-core/src/server/instanceEnvironment.ts index 0ddb61af4b5c..c4dfe37cbb8c 100644 --- a/packages/provider-core/src/server/instanceEnvironment.ts +++ b/packages/provider-core/src/server/instanceEnvironment.ts @@ -1,5 +1,5 @@ import type { ProviderInstanceEnvironment } from "@t3tools/contracts"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import { expandHomePath } from "./pathExpansion.ts"; @@ -12,7 +12,7 @@ export const mergeProviderInstanceEnvironment = Effect.fn(function* ( return baseEnv; } - const home = yield* HostProcessHomeDirectory; + const home = yield* HostProcess.HomeDirectory; const next: NodeJS.ProcessEnv = { ...baseEnv }; for (const variable of environment) { // Child processes do not apply shell expansion to environment values. diff --git a/packages/provider-core/src/server/maintenanceResolver.test.ts b/packages/provider-core/src/server/maintenanceResolver.test.ts index 23f0707a4a51..b0b1be74ee88 100644 --- a/packages/provider-core/src/server/maintenanceResolver.test.ts +++ b/packages/provider-core/src/server/maintenanceResolver.test.ts @@ -6,7 +6,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import * as NodeOS from "node:os"; import * as NodePath from "node:path"; import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Crypto from "effect/Crypto"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; @@ -37,7 +37,7 @@ import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; const driver = (value: string) => ProviderDriverKind.make(value); // These write `#!/bin/sh` stubs and evaluate them with darwin/linux path // semantics; a Windows temp path cannot be split on `:`. -const windowsHost = HostProcessPlatform.defaultValue() === "win32"; +const windowsHost = HostProcess.Platform.defaultValue() === "win32"; const makeTempDir = (name: string) => Crypto.Crypto.pipe( Effect.flatMap((crypto) => crypto.randomUUIDv4), @@ -332,7 +332,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => { env: {}, platform: "win32", }, - ).pipe(Effect.provideService(HostProcessPlatform, "win32")); + ).pipe(Effect.provideService(HostProcess.Platform, "win32")); expect(capabilities.update).toMatchObject({ executable: visiblePath, @@ -360,7 +360,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => { binaryPath: shim, env: { PATH: "", PATHEXT: ".COM;.EXE;.BAT;.CMD" }, }).pipe( - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, noSpawn), ); @@ -376,7 +376,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => { binaryPath: script, env: { PATH: "" }, }).pipe( - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, noSpawn), ); expect(posix.update).toBeNull(); @@ -429,7 +429,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => { env: { PATH: bunBinDir }, }, ).pipe( - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Platform, "darwin"), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, noSpawn), ); @@ -454,7 +454,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => { env: { PATH: nativeBinDir }, }, ).pipe( - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Platform, "darwin"), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, noSpawn), ); @@ -659,7 +659,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => { env: { PATH: brewBinDir }, }, ).pipe( - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Platform, "darwin"), Effect.provideService( ChildProcessSpawner.ChildProcessSpawner, stdoutSpawner((command, args) => { @@ -725,7 +725,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => { env: { PATH: brewBinDir }, }, ).pipe( - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Platform, "darwin"), Effect.provideService( ChildProcessSpawner.ChildProcessSpawner, stdoutSpawner(() => "/opt/homebrew\n"), @@ -864,7 +864,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => { binaryPath: kegBinary, env: { PATH: "" }, }).pipe( - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Platform, "darwin"), Effect.provideService( ChildProcessSpawner.ChildProcessSpawner, stdoutSpawner((_command, args) => (args[0] === "--prefix" ? `${tempDir}\n` : "{}")), diff --git a/packages/provider-core/src/server/maintenanceResolver.ts b/packages/provider-core/src/server/maintenanceResolver.ts index fb985bd2011a..c625f067bd45 100644 --- a/packages/provider-core/src/server/maintenanceResolver.ts +++ b/packages/provider-core/src/server/maintenanceResolver.ts @@ -12,7 +12,7 @@ import { type ServerProviderVersionAdvisory, } from "@t3tools/contracts"; import { compareSemverVersions } from "@t3tools/shared/semver"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { causeErrorTag } from "@t3tools/shared/observability"; import { resolveCommandPath } from "@t3tools/shared/shell"; import * as Cache from "effect/Cache"; @@ -141,7 +141,7 @@ export function makeProviderMaintenanceCapabilities(input: { readonly env?: NodeJS.ProcessEnv; readonly latestVersion?: string | null; }): ProviderMaintenanceCapabilities { - const platform = input.platform ?? HostProcessPlatform.defaultValue(); + const platform = input.platform ?? HostProcess.Platform.defaultValue(); const update = input.updateExecutable === null || input.updateLockKey === null ? null @@ -587,7 +587,7 @@ const resolveNpmGlobalPrefix = Effect.fn("resolveNpmGlobalPrefix")(function* ( if (fromRealPath) { return fromRealPath; } - if ((yield* HostProcessPlatform) !== "win32") { + if ((yield* HostProcess.Platform) !== "win32") { return null; } const fileSystem = yield* FileSystem.FileSystem; @@ -667,7 +667,7 @@ export const resolveProviderMaintenanceCapabilitiesEffect = Effect.fn( resolvedCommandPath, realCommandPath, env, - platform: yield* HostProcessPlatform, + platform: yield* HostProcess.Platform, }); }); diff --git a/packages/provider-core/src/server/pathExpansion.ts b/packages/provider-core/src/server/pathExpansion.ts index d486b2988d47..1cc52dd0e9d7 100644 --- a/packages/provider-core/src/server/pathExpansion.ts +++ b/packages/provider-core/src/server/pathExpansion.ts @@ -3,7 +3,7 @@ import * as NodePath from "node:path"; /** * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to `home`, - * the current user's home directory (`HostProcessHomeDirectory`). Spawned + * the current user's home directory (`HostProcess.HomeDirectory`). Spawned * processes don't get shell expansion, so env vars like * `CODEX_HOME=~/.codex-work` would be passed verbatim and treated as relative * paths by the receiver. diff --git a/packages/provider-core/src/server/processGroup.test.ts b/packages/provider-core/src/server/processGroup.test.ts index 9694d9b7ab94..b2b39abe4320 100644 --- a/packages/provider-core/src/server/processGroup.test.ts +++ b/packages/provider-core/src/server/processGroup.test.ts @@ -2,7 +2,7 @@ import * as NodeChildProcess from "node:child_process"; import { describe, expect, it } from "@effect/vitest"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { signalProcessGroup } from "./processGroup.ts"; @@ -15,7 +15,7 @@ const errorCode = (run: () => void) => { } }; -describe.skipIf(HostProcessPlatform.defaultValue() === "win32")("signalProcessGroup", () => { +describe.skipIf(HostProcess.Platform.defaultValue() === "win32")("signalProcessGroup", () => { // Signal 0 only probes, so this is safe to run unguarded: `kill(-1, 0)` and // `kill(0, 0)` succeed, which is how a fake spawner's pid 1 became // `kill(-1, SIGKILL)` and took down every process the user owned. diff --git a/packages/provider-core/src/server/snapshotProbe.test.ts b/packages/provider-core/src/server/snapshotProbe.test.ts index 8ee272cee394..fc207f90f340 100644 --- a/packages/provider-core/src/server/snapshotProbe.test.ts +++ b/packages/provider-core/src/server/snapshotProbe.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "@effect/vitest"; import type { ModelCapabilities } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { createModelCapabilities } from "@t3tools/shared/model"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -158,7 +158,7 @@ describe("ProviderCommandNotFoundError", () => { ChildProcess.make("codex", ["--version"]), ).pipe( Effect.provide(Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner)), - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), Effect.flip, ); diff --git a/packages/provider-core/src/server/snapshotProbe.ts b/packages/provider-core/src/server/snapshotProbe.ts index f61881ba0806..071d9fd5ae07 100644 --- a/packages/provider-core/src/server/snapshotProbe.ts +++ b/packages/provider-core/src/server/snapshotProbe.ts @@ -19,7 +19,7 @@ import * as Stream from "effect/Stream"; import * as ChildProcess from "effect/process/ChildProcess"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; import { readCustomModelEntries } from "@t3tools/shared/model"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { createProviderVersionAdvisory } from "./maintenanceResolver.ts"; import { collectUint8StreamText } from "./collectStreamText.ts"; @@ -38,7 +38,7 @@ export const isWindowsCommandNotFound = Effect.fn("isWindowsCommandNotFound")(fu code: number | null, stderr: string, ) { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; if (platform !== "win32") return false; if (code === 9009) return true; return WINDOWS_COMMAND_NOT_FOUND_PATTERNS.some((pattern) => pattern.test(stderr)); diff --git a/packages/provider-cursor/src/server/CursorUsageAccounts.ts b/packages/provider-cursor/src/server/CursorUsageAccounts.ts index 56f64104a449..24f4ff87afff 100644 --- a/packages/provider-cursor/src/server/CursorUsageAccounts.ts +++ b/packages/provider-cursor/src/server/CursorUsageAccounts.ts @@ -14,11 +14,7 @@ import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; import type { ProviderUsageScan } from "@t3tools/provider-core/server/usage"; import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite"; -import { - HostProcessEnvironment, - HostProcessHomeDirectory, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; import * as Deferred from "effect/Deferred"; @@ -71,8 +67,8 @@ const make = Effect.gen(function* () { const path = yield* Path.Path; const host = yield* ProviderHost.ProviderHost; const accountReader = yield* CursorAccountReader.CursorAccountReader; - const hostEnvironment = yield* HostProcessEnvironment; - const platform = yield* HostProcessPlatform; + const hostEnvironment = yield* HostProcess.Environment; + const platform = yield* HostProcess.Platform; const cachePath = path.join(host.paths.stateDir, CURSOR_ACCOUNT_CACHE_FILE_NAME); /** Account caches by credential source. */ @@ -286,7 +282,7 @@ const make = Effect.gen(function* () { const scan: CursorUsageAccounts["Service"]["scan"] = Effect.fn("CursorUsageAccounts.scan")( function* ({ keychainUsageEnabled, windowStartMs, retentionCutoffMs, awaitRefresh }) { - const home = yield* HostProcessHomeDirectory; + const home = yield* HostProcess.HomeDirectory; const userHome = (platform === "win32" ? hostEnvironment["USERPROFILE"] : hostEnvironment["HOME"]) || home; const configHome = hostEnvironment["XDG_CONFIG_HOME"]?.trim(); diff --git a/packages/provider-cursor/src/server/adapter.ts b/packages/provider-cursor/src/server/adapter.ts index fa557d6d8bc1..f7cd8005eef5 100644 --- a/packages/provider-cursor/src/server/adapter.ts +++ b/packages/provider-cursor/src/server/adapter.ts @@ -9,7 +9,7 @@ import type { ToolCall, } from "@cursor/sdk"; import { formatReadToolLabel, formatSearchToolLabel } from "@t3tools/shared/toolActivity"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { isOrchestrationV2WorkActive, defaultInstanceIdForDriver, @@ -2620,7 +2620,7 @@ export const CursorAdapterV2Driver: ProviderAdapterDriver< defaultConfig: (): CursorSettings => DEFAULT_CURSOR_SETTINGS, create: Effect.fn("CursorAdapterV2Driver.create")( function* (input: ProviderAdapterDriverCreateInput) { - const hostEnvironment = yield* HostProcessEnvironment; + const hostEnvironment = yield* HostProcess.Environment; return yield* makeCursorAdapterV2({ instanceId: input.instanceId, settings: { diff --git a/packages/provider-cursor/src/server/skills.ts b/packages/provider-cursor/src/server/skills.ts index 4c7b0bbea985..371aa75352d6 100644 --- a/packages/provider-cursor/src/server/skills.ts +++ b/packages/provider-cursor/src/server/skills.ts @@ -17,7 +17,7 @@ import * as Path from "effect/Path"; import * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; import { parse as parseYamlDocument } from "yaml"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/; const SKILL_MENTION_PATTERN = @@ -224,7 +224,7 @@ const inspectCursorSkills = Effect.fn("inspectCursorSkills")(function* ( const userHome = environment.HOME?.trim() || environment.USERPROFILE?.trim() || - (yield* HostProcessHomeDirectory); + (yield* HostProcess.HomeDirectory); const rootsBelow = (base: string, scope: "user" | "project") => [ { directory: path.join(base, ".cursor", "skills"), scope }, { directory: path.join(base, ".agents", "skills"), scope }, diff --git a/packages/provider-cursor/src/server/textGeneration.ts b/packages/provider-cursor/src/server/textGeneration.ts index 6a6b8ae989cf..ff1325d648d3 100644 --- a/packages/provider-cursor/src/server/textGeneration.ts +++ b/packages/provider-cursor/src/server/textGeneration.ts @@ -12,7 +12,7 @@ import * as TextGenerationOperations from "@t3tools/provider-core/server/textGen import { cursorSdkModelSelection } from "./sdkModel.ts"; import type { CursorAuth } from "./auth.ts"; import * as CursorSdk from "./CursorSdk.ts"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; const CURSOR_TIMEOUT_MS = 180_000; @@ -90,7 +90,7 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu const apiKey = yield* resolveCursorApiKey(operation); // The SDK loads sandbox.json independently of settingSources and lets it // expand the writable paths. Its public API cannot override that policy. - if (yield* fs.exists(`${yield* HostProcessHomeDirectory}/.cursor/sandbox.json`)) { + if (yield* fs.exists(`${yield* HostProcess.HomeDirectory}/.cursor/sandbox.json`)) { return yield* new TextGenerationError({ operation, detail: diff --git a/packages/provider-cursor/src/server/usageLimits.test.ts b/packages/provider-cursor/src/server/usageLimits.test.ts index bb3e38703037..ce8a2cbc27cf 100644 --- a/packages/provider-cursor/src/server/usageLimits.test.ts +++ b/packages/provider-cursor/src/server/usageLimits.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "@effect/vitest"; import { NodeServices } from "@effect/platform-node"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; @@ -106,7 +106,7 @@ describe("Cursor usage limits", () => { { apiEndpoint: "https://cursor.example/" }, { XDG_CONFIG_HOME: directory, HOME: directory, AGENT_CLI_CREDENTIAL_STORE: "file" }, ).pipe( - Effect.provideService(HostProcessPlatform, platform), + Effect.provideService(HostProcess.Platform, platform), Effect.provideService(HttpClient.HttpClient, client), ); expect(limits.windows[0]?.usedPercent).toBe(42); @@ -131,7 +131,7 @@ describe("Cursor usage limits", () => { }, false, ).pipe( - Effect.provideService(HostProcessPlatform, platform), + Effect.provideService(HostProcess.Platform, platform), Effect.provideService( FileSystem.FileSystem, FileSystem.makeNoop({ @@ -202,7 +202,7 @@ describe("Cursor usage limits", () => { Effect.gen(function* () { const limits = yield* withNodeServices( readCursorUsageLimits({ apiEndpoint: "" }, {}, true).pipe( - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Platform, "darwin"), Effect.provideService( FileSystem.FileSystem, FileSystem.makeNoop({ @@ -232,7 +232,7 @@ describe("Cursor usage limits", () => { Effect.gen(function* () { const limits = yield* withNodeServices( readCursorUsageLimits({ apiEndpoint: "" }, {}, true).pipe( - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Platform, "darwin"), Effect.provideService( HttpClient.HttpClient, HttpClient.make(() => Effect.die("must not request limits without a login")), @@ -258,7 +258,7 @@ describe("Cursor usage limits", () => { ] as const) { const limits = yield* withNodeServices( readCursorUsageLimits({ apiEndpoint }, environment, true).pipe( - Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(HostProcess.Platform, "darwin"), Effect.provideService( HttpClient.HttpClient, HttpClient.make(() => Effect.die("must not send a Keychain credential to a proxy")), diff --git a/packages/provider-cursor/src/server/usageLimits.ts b/packages/provider-cursor/src/server/usageLimits.ts index dd29a443cd3a..049e646d2c49 100644 --- a/packages/provider-cursor/src/server/usageLimits.ts +++ b/packages/provider-cursor/src/server/usageLimits.ts @@ -1,6 +1,6 @@ import type { ServerProviderUsageWindow } from "@t3tools/contracts"; import type { CursorSettings } from "../settings.ts"; -import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { CURSOR_USAGE_WINDOWS } from "@t3tools/shared/usageLimits"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; @@ -70,7 +70,7 @@ export const readCursorUsageLimits = Effect.fn("readCursorUsageLimits")(function return yield* Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const endpoint = ( settings.apiEndpoint?.trim() || environment.CURSOR_API_ENDPOINT?.trim() || @@ -109,7 +109,7 @@ export const readCursorUsageLimits = Effect.fn("readCursorUsageLimits")(function } else if (!token) { const home = (platform === "win32" ? environment.USERPROFILE : environment.HOME) || - (yield* HostProcessHomeDirectory); + (yield* HostProcess.HomeDirectory); const directory = platform === "win32" ? path.join(environment.APPDATA || path.join(home, "AppData", "Roaming"), "Cursor") diff --git a/packages/provider-grok/src/server/acpSupport.ts b/packages/provider-grok/src/server/acpSupport.ts index 53f5ae3ffb2b..60d395284648 100644 --- a/packages/provider-grok/src/server/acpSupport.ts +++ b/packages/provider-grok/src/server/acpSupport.ts @@ -12,7 +12,7 @@ import * as Layer from "effect/Layer"; import * as Scope from "effect/Scope"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; import * as EffectAcpErrors from "effect-acp/errors"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { normalizeModelSlug } from "@t3tools/shared/model"; import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime"; @@ -159,7 +159,7 @@ export const makeGrokAcpRuntime = ( ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope > => Effect.gen(function* () { - const processGroupPlatform = yield* HostProcessPlatform.pipe( + const processGroupPlatform = yield* HostProcess.Platform.pipe( Effect.provide(NodeServices.layer), ); const acpContext = yield* Layer.build( diff --git a/packages/provider-grok/src/server/adapter.ts b/packages/provider-grok/src/server/adapter.ts index cca040e81698..224654585625 100644 --- a/packages/provider-grok/src/server/adapter.ts +++ b/packages/provider-grok/src/server/adapter.ts @@ -4,11 +4,7 @@ import { isXAiTaskCompletedWakeNotification, xAiRateLimitedErrorCode, } from "./xaiAcpExtension.ts"; -import { - HostProcessEnvironment, - HostProcessHomeDirectory, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveSelfInvocation, type SelfInvocation } from "@t3tools/shared/nodeRuntime"; import { defaultInstanceIdForDriver, @@ -339,7 +335,7 @@ export const makeGrokAdapterV2 = Effect.fn("makeGrokAdapterV2")(function* ( ) { return yield* makeAcpAdapterV2({ instanceId: options.instanceId, - flavor: makeGrokAcpAdapterFlavor(options, yield* HostProcessHomeDirectory), + flavor: makeGrokAcpAdapterFlavor(options, yield* HostProcess.HomeDirectory), selfInvocation: options.selfInvocation, ...(options.nativeLogging === undefined ? {} : { nativeLogging: options.nativeLogging }), ...(options.continuationRequests === undefined @@ -365,8 +361,8 @@ export const GrokAdapterV2Driver: ProviderAdapterDriver DEFAULT_GROK_SETTINGS, create: Effect.fn("GrokAdapterV2Driver.create")( function* (input: ProviderAdapterDriverCreateInput) { - const hostEnvironment = yield* HostProcessEnvironment; - const hostPlatform = yield* HostProcessPlatform; + const hostEnvironment = yield* HostProcess.Environment; + const hostPlatform = yield* HostProcess.Platform; const selfInvocation = yield* resolveSelfInvocation(); const providerEventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers; const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; @@ -415,8 +411,8 @@ const layer: Layer.Layer< > = Layer.effect( ProviderAdapter.ProviderAdapterV2, Effect.gen(function* () { - const hostEnvironment = yield* HostProcessEnvironment; - const hostPlatform = yield* HostProcessPlatform; + const hostEnvironment = yield* HostProcess.Environment; + const hostPlatform = yield* HostProcess.Platform; const selfInvocation = yield* resolveSelfInvocation(); const providerEventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers; const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; diff --git a/packages/provider-grok/src/server/driver.test.ts b/packages/provider-grok/src/server/driver.test.ts index 22b2c64aa0b8..b3e96bad0d04 100644 --- a/packages/provider-grok/src/server/driver.test.ts +++ b/packages/provider-grok/src/server/driver.test.ts @@ -1,7 +1,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { expect, it } from "@effect/vitest"; import { ProviderInstanceId } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; @@ -41,7 +41,7 @@ const noSpawner = ChildProcessSpawner.make(() => ); // The `#!/bin/sh` stub below cannot be resolved as an executable on Windows. -const windowsHost = HostProcessPlatform.defaultValue() === "win32"; +const windowsHost = HostProcess.Platform.defaultValue() === "win32"; it.layer(layerTest)("GrokDriver", (it) => { it.effect.skipIf(windowsHost)("updates through the configured executable's own updater", () => diff --git a/packages/provider-grok/src/server/usage.ts b/packages/provider-grok/src/server/usage.ts index 8b5c0db45ea6..4359cd11959c 100644 --- a/packages/provider-grok/src/server/usage.ts +++ b/packages/provider-grok/src/server/usage.ts @@ -19,7 +19,7 @@ import { } from "@t3tools/provider-core/server/usage"; import type { GrokSettings } from "../settings.ts"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; /** * Grok reports cost in integer ticks where `1 USD = 10^10` ticks. See Grok @@ -229,7 +229,7 @@ export const grokUsageReader: ProviderUsageReader = { format: grokUsageFormat, directories: Effect.fn("grokUsageReader.directories")(function* ({ environment }) { const path = yield* Path.Path; - const homeDirectory = yield* HostProcessHomeDirectory; + const homeDirectory = yield* HostProcess.HomeDirectory; const home = expandHomePath( environment.GROK_HOME?.trim() || path.join(homeDirectory, ".grok"), homeDirectory, diff --git a/packages/provider-grok/src/server/usageLimits.ts b/packages/provider-grok/src/server/usageLimits.ts index b7db7f6fd79a..d4a855883a6c 100644 --- a/packages/provider-grok/src/server/usageLimits.ts +++ b/packages/provider-grok/src/server/usageLimits.ts @@ -13,7 +13,7 @@ import { makeUnavailableUsageLimits, makeUsageLimits, } from "@t3tools/provider-core/server/usageLimits"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; const GrokCredentials = Schema.Record( Schema.String, @@ -101,7 +101,7 @@ const readGrokCredential = Effect.fn("readGrokCredential")(function* ( const home = environment.GROK_HOME?.trim() || path.join( - environment.HOME || environment.USERPROFILE || (yield* HostProcessHomeDirectory), + environment.HOME || environment.USERPROFILE || (yield* HostProcess.HomeDirectory), ".grok", ); for (const configPath of [ diff --git a/packages/provider-muse/src/server/driver.ts b/packages/provider-muse/src/server/driver.ts index 3b9df42100a6..9a70f74eed83 100644 --- a/packages/provider-muse/src/server/driver.ts +++ b/packages/provider-muse/src/server/driver.ts @@ -1,6 +1,6 @@ import { ProviderDriverKind } from "@t3tools/contracts"; import { MuseSettings } from "../settings.ts"; -import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; @@ -69,7 +69,7 @@ export const MuseDriver: ProviderDriver = { const eventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers; const { cwd } = host.paths; const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; - const hostEnvironment = yield* HostProcessEnvironment; + const hostEnvironment = yield* HostProcess.Environment; // Drop an inherited META_API_KEY so Muse uses its login; an instance value still wins. const processEnvironment = yield* mergeProviderInstanceEnvironment( environment, @@ -78,7 +78,7 @@ export const MuseDriver: ProviderDriver = { const effectiveConfig = { ...config, enabled, - binaryPath: expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory), + binaryPath: expandHomePath(config.binaryPath, yield* HostProcess.HomeDirectory), } satisfies MuseSettings; const continuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, diff --git a/packages/provider-opencode/src/server/OpenCodeRuntime.environment.test.ts b/packages/provider-opencode/src/server/OpenCodeRuntime.environment.test.ts index 6476c7a7c3ce..5e2746d0f82c 100644 --- a/packages/provider-opencode/src/server/OpenCodeRuntime.environment.test.ts +++ b/packages/provider-opencode/src/server/OpenCodeRuntime.environment.test.ts @@ -1,11 +1,7 @@ import type { OpencodeClient } from "@opencode-ai/sdk/v2"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { it as effectIt } from "@effect/vitest"; -import { - HostProcessEnvironment, - HostProcessExecutablePath, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; @@ -168,9 +164,9 @@ describe("OpenCode server output", () => { Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const environment = yield* HostProcessEnvironment; - const executablePath = yield* HostProcessExecutablePath; - const platform = yield* HostProcessPlatform; + const environment = yield* HostProcess.Environment; + const executablePath = yield* HostProcess.ExecutablePath; + const platform = yield* HostProcess.Platform; const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-opencode-output-" }); const isWindows = platform === "win32"; const binaryPath = path.join(tempDir, isWindows ? "opencode.cmd" : "opencode"); diff --git a/packages/provider-opencode/src/server/OpenCodeRuntime.inventory.test.ts b/packages/provider-opencode/src/server/OpenCodeRuntime.inventory.test.ts index 29e302c134f1..d9cdfa9e6441 100644 --- a/packages/provider-opencode/src/server/OpenCodeRuntime.inventory.test.ts +++ b/packages/provider-opencode/src/server/OpenCodeRuntime.inventory.test.ts @@ -9,11 +9,7 @@ import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; import * as Queue from "effect/Queue"; -import { - HostProcessEnvironment, - HostProcessExecutablePath, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as OpenCodeRuntime from "./OpenCodeRuntime.ts"; import * as OpenCodeServerLedger from "./OpenCodeServerLedger.ts"; @@ -217,9 +213,9 @@ it.layer(layerTest)("OpenCodeRuntime inventory", (it) => { Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const hostEnvironment = yield* HostProcessEnvironment; - const executablePath = yield* HostProcessExecutablePath; - const hostPlatform = yield* HostProcessPlatform; + const hostEnvironment = yield* HostProcess.Environment; + const executablePath = yield* HostProcess.ExecutablePath; + const hostPlatform = yield* HostProcess.Platform; const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-opencode-inventory-" }); const isWindows = hostPlatform === "win32"; const binaryPath = path.join(tempDir, isWindows ? "opencode.cmd" : "opencode"); @@ -271,7 +267,7 @@ it.layer(layerTest)("OpenCodeRuntime inventory", (it) => { it.effect("caps and drains command stdout and stderr when requested", () => Effect.gen(function* () { const runtime = yield* OpenCodeRuntime.OpenCodeRuntime; - const executablePath = yield* HostProcessExecutablePath; + const executablePath = yield* HostProcess.ExecutablePath; const outputBytes = 2 * 1024 * 1024; const result = yield* runtime.runOpenCodeCommand({ binaryPath: executablePath, diff --git a/packages/provider-opencode/src/server/OpenCodeRuntime.ts b/packages/provider-opencode/src/server/OpenCodeRuntime.ts index 4637ec831b21..c91da0326228 100644 --- a/packages/provider-opencode/src/server/OpenCodeRuntime.ts +++ b/packages/provider-opencode/src/server/OpenCodeRuntime.ts @@ -36,7 +36,7 @@ import { isWindowsCommandNotFound } from "@t3tools/provider-core/server/snapshot import * as OpenCodeServerLedger from "./OpenCodeServerLedger.ts"; import { collectStreamAsString } from "@t3tools/provider-core/server/snapshotProbe"; import * as NetService from "@t3tools/shared/Net"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { compareSemverVersions, parseSemver } from "@t3tools/shared/semver"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.fromJsonString(Schema.Unknown)); @@ -595,7 +595,7 @@ function ensureRuntimeError( const makeOpenCodeRuntime = Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const netService = yield* NetService.NetService; - const hostPlatform = yield* HostProcessPlatform; + const hostPlatform = yield* HostProcess.Platform; const serverLedger = yield* OpenCodeServerLedger.OpenCodeServerLedger; const resolveCommand = (command: string, args: ReadonlyArray, env?: NodeJS.ProcessEnv) => resolveSpawnCommand(command, args, env ? { env } : {}); diff --git a/packages/provider-opencode/src/server/OpenCodeServerLedger.test.ts b/packages/provider-opencode/src/server/OpenCodeServerLedger.test.ts index 90b0c55c157f..45bb5bb5d7c1 100644 --- a/packages/provider-opencode/src/server/OpenCodeServerLedger.test.ts +++ b/packages/provider-opencode/src/server/OpenCodeServerLedger.test.ts @@ -3,7 +3,7 @@ import * as NodeChildProcess from "node:child_process"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { describe, expect, it } from "@effect/vitest"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; @@ -67,7 +67,7 @@ const recordFromDeadServer = (stateDir: string, server: { readonly pid: number } yield* previousServer.exited; }); -const hostPlatform = HostProcessPlatform.defaultValue(); +const hostPlatform = HostProcess.Platform.defaultValue(); // procps accepts the same `ps` flags as macOS, so Linux also covers the macOS path. const observedPlatforms: ReadonlyArray = hostPlatform === "linux" ? ["linux", "darwin"] : hostPlatform === "darwin" ? ["darwin"] : []; @@ -75,7 +75,7 @@ const observedPlatforms: ReadonlyArray = describe.each(observedPlatforms)("OpenCodeServerLedger observing as %s", (platform) => { const provideHost = (effect: Effect.Effect) => effect.pipe( - Effect.provideService(HostProcessPlatform, platform), + Effect.provideService(HostProcess.Platform, platform), Effect.provide(NodeServices.layer), ); diff --git a/packages/provider-opencode/src/server/OpenCodeServerLedger.ts b/packages/provider-opencode/src/server/OpenCodeServerLedger.ts index 43774e245232..cb6b2f92da41 100644 --- a/packages/provider-opencode/src/server/OpenCodeServerLedger.ts +++ b/packages/provider-opencode/src/server/OpenCodeServerLedger.ts @@ -1,4 +1,4 @@ -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -119,7 +119,7 @@ export const make = Effect.fn("OpenCodeServerLedger.make")(function* (input: { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const directory = path.join(input.stateDir, ENTRY_DIRECTORY); // Windows servers are not detached and are never recorded. const recordable = platform === "linux" || platform === "darwin"; diff --git a/packages/provider-opencode/src/server/adapter.ts b/packages/provider-opencode/src/server/adapter.ts index c7020b8ff45a..327d4d8cef4f 100644 --- a/packages/provider-opencode/src/server/adapter.ts +++ b/packages/provider-opencode/src/server/adapter.ts @@ -9,7 +9,7 @@ import type { Todo as OpenCodeTodo, ToolPart, } from "@opencode-ai/sdk/v2"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { getModelSelectionStringOptionValue } from "@t3tools/shared/model"; import { causeErrorTag } from "@t3tools/shared/observability"; import { @@ -3739,7 +3739,7 @@ export const OpenCodeAdapterV2Driver: ProviderAdapterDriver< defaultConfig: (): OpenCodeSettings => DEFAULT_OPENCODE_SETTINGS, create: Effect.fn("OpenCodeAdapterV2Driver.create")( function* (input: ProviderAdapterDriverCreateInput) { - const hostEnvironment = yield* HostProcessEnvironment; + const hostEnvironment = yield* HostProcess.Environment; const providerEventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers; return yield* makeOpenCodeAdapterV2({ instanceId: input.instanceId, @@ -3769,7 +3769,7 @@ const layer: Layer.Layer Effect.provide(NodeServices.layer), ); -it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")( +it.effect.skipIf(HostProcess.Platform.defaultValue() === "win32")( "updates an npm-global OpenCode 2 install as @opencode/cli and a 1.x one as opencode-ai", () => Effect.gen(function* () { @@ -243,7 +243,7 @@ it.layer(layerUpdate)("OpenCodeDriver updates", (it) => { }).pipe(Effect.scoped), ); - it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")( + it.effect.skipIf(HostProcess.Platform.defaultValue() === "win32")( "offers no package update for an unknown version and follows a changed one on a fresh read", () => Effect.gen(function* () { diff --git a/packages/provider-opencode/src/server/usage.ts b/packages/provider-opencode/src/server/usage.ts index 65897f85d98e..9d09266118eb 100644 --- a/packages/provider-opencode/src/server/usage.ts +++ b/packages/provider-opencode/src/server/usage.ts @@ -11,7 +11,7 @@ import { type ProviderUsageReader, type UsageRecord, } from "@t3tools/provider-core/server/usage"; -import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; @@ -257,8 +257,8 @@ export const readOpenCodeUsage = Effect.fn("readOpenCodeUsage")(function* ( const resolveOpenCodeDataDirs = Effect.fn("resolveOpenCodeDataDirs")(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const environment = yield* HostProcessEnvironment; - const homeDirectory = yield* HostProcessHomeDirectory; + const environment = yield* HostProcess.Environment; + const homeDirectory = yield* HostProcess.HomeDirectory; const roots = environment["OPENCODE_DATA_DIR"] ?.split(",") .map((value) => value.trim()) diff --git a/packages/provider-opencode/src/server/usageLimits.ts b/packages/provider-opencode/src/server/usageLimits.ts index 05c3b79d411a..6472bb71039c 100644 --- a/packages/provider-opencode/src/server/usageLimits.ts +++ b/packages/provider-opencode/src/server/usageLimits.ts @@ -16,7 +16,7 @@ import { makeUnavailableUsageLimits, makeUsageLimits, } from "@t3tools/provider-core/server/usageLimits"; -import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; const AuthFile = Schema.Struct({ "opencode-go": Schema.optionalKey(Schema.Unknown) }); const ApiAuth = Schema.Struct({ type: Schema.Literal("api"), key: Schema.String }); @@ -47,7 +47,7 @@ export const readOpenCodeGoUsageLimits = Effect.fn("readOpenCodeGoUsageLimits")( const dataHome = env.XDG_DATA_HOME || path.join( - env.HOME || env.USERPROFILE || (yield* HostProcessHomeDirectory), + env.HOME || env.USERPROFILE || (yield* HostProcess.HomeDirectory), ".local", "share", ); diff --git a/packages/provider-opencode/src/server/v2/OpenCode2Server.test.ts b/packages/provider-opencode/src/server/v2/OpenCode2Server.test.ts index 340e03f8d7d5..9fab7f63420d 100644 --- a/packages/provider-opencode/src/server/v2/OpenCode2Server.test.ts +++ b/packages/provider-opencode/src/server/v2/OpenCode2Server.test.ts @@ -1,10 +1,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; -import { - HostProcessEnvironment, - HostProcessExecutablePath, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; @@ -221,7 +217,7 @@ describe("OpenCode2Server spawned server", () => { Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-opencode2-fake-" }); const isWindows = platform === "win32"; const binaryPath = path.join(directory, isWindows ? "opencode.cmd" : "opencode"); @@ -230,8 +226,8 @@ describe("OpenCode2Server spawned server", () => { yield* fs.writeFileString( binaryPath, isWindows - ? `@echo off\r\n"${yield* HostProcessExecutablePath}" "${scriptPath}" %*\r\n` - : `#!/bin/sh\nexec "${yield* HostProcessExecutablePath}" "${scriptPath}" "$@"\n`, + ? `@echo off\r\n"${yield* HostProcess.ExecutablePath}" "${scriptPath}" %*\r\n` + : `#!/bin/sh\nexec "${yield* HostProcess.ExecutablePath}" "${scriptPath}" "$@"\n`, ); if (!isWindows) yield* fs.chmod(binaryPath, 0o755); @@ -240,7 +236,7 @@ describe("OpenCode2Server spawned server", () => { serverUrl: "", serverPassword: "", directory, - environment: { ...(yield* HostProcessEnvironment), OPENCODE_SERVER_PASSWORD: "ambient" }, + environment: { ...(yield* HostProcess.Environment), OPENCODE_SERVER_PASSWORD: "ambient" }, }); const first = yield* server.withConnection((connection) => Effect.succeed(connection)); const second = yield* server.withConnection((connection) => Effect.succeed(connection)); diff --git a/packages/provider-pi/src/server/adapter.test.ts b/packages/provider-pi/src/server/adapter.test.ts index 8508fb064837..e13f8213077f 100644 --- a/packages/provider-pi/src/server/adapter.test.ts +++ b/packages/provider-pi/src/server/adapter.test.ts @@ -38,7 +38,7 @@ import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderS import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter"; import { handoffBudget } from "@t3tools/provider-core/server/handoffBudget"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { makePiAdapterV2, PiAdapterV2Driver, @@ -336,7 +336,7 @@ const makeAdapter = Effect.fnUntraced(function* ( config: { enabled: true, binaryPath: "pi", launchArgs, customModels: [] }, }).pipe( Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), - Effect.provideService(HostProcessEnvironment, {}), + Effect.provideService(HostProcess.Environment, {}), Effect.provideService(ProviderContinuationRequests.ProviderContinuationRequests, { ...continuationRequests, take: Effect.never, diff --git a/packages/provider-pi/src/server/adapter.ts b/packages/provider-pi/src/server/adapter.ts index c6bdc4338ab3..c650a444daf9 100644 --- a/packages/provider-pi/src/server/adapter.ts +++ b/packages/provider-pi/src/server/adapter.ts @@ -23,7 +23,7 @@ * Terminal-only decoration such as status, widget, title, and editor-text * updates has no matching T3 surface and is ignored. */ -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { getModelSelectionStringOptionValue } from "@t3tools/shared/model"; import { defaultInstanceIdForDriver, @@ -3271,7 +3271,7 @@ export const PiAdapterV2Driver: ProviderAdapterDriver DEFAULT_PI_SETTINGS, create: Effect.fn("PiAdapterV2Driver.create")( function* (input: ProviderAdapterDriverCreateInput) { - const hostEnvironment = yield* HostProcessEnvironment; + const hostEnvironment = yield* HostProcess.Environment; const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; return yield* makePiAdapterV2({ instanceId: input.instanceId, @@ -3299,7 +3299,7 @@ const layer: Layer.Layer boolean, hasExited export const makePiRpcConnection = Effect.fnUntraced(function* (options: PiRpcSpawnOptions) { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const scope = yield* Effect.scope; const spawnCommand = yield* resolveSpawnCommand(options.command, [...options.args], { diff --git a/packages/provider-testing/src/fakeCli.ts b/packages/provider-testing/src/fakeCli.ts index 68ab157aed7a..650f2fd0d5e2 100644 --- a/packages/provider-testing/src/fakeCli.ts +++ b/packages/provider-testing/src/fakeCli.ts @@ -2,7 +2,7 @@ import * as NodeFS from "node:fs"; import * as NodePath from "node:path"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; export interface FakeCliOptions { /** Directory the launcher and its stub are written into. */ @@ -15,7 +15,7 @@ export interface FakeCliOptions { readonly env?: Readonly>; /** * Platform the launcher is shaped for. Effect callers should pass the value - * they read from `HostProcessPlatform` so an injected override is honoured; + * they read from `HostProcess.Platform` so an injected override is honoured; * defaults to the real host for plain tests. */ readonly platform?: NodeJS.Platform; @@ -45,7 +45,7 @@ export function writeFakeCli(options: FakeCliOptions): string { "utf8", ); - if ((options.platform ?? HostProcessPlatform.defaultValue()) === "win32") { + if ((options.platform ?? HostProcess.Platform.defaultValue()) === "win32") { const launcherPath = NodePath.join(options.directory, `${options.name}.cmd`); NodeFS.writeFileSync( launcherPath, diff --git a/packages/shared/package.json b/packages/shared/package.json index fdc368b6c399..fd68e8dada27 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -311,9 +311,9 @@ "types": "./src/nodeRuntime.ts", "import": "./src/nodeRuntime.ts" }, - "./hostProcess": { - "types": "./src/hostProcess.ts", - "import": "./src/hostProcess.ts" + "./HostProcess": { + "types": "./src/HostProcess.ts", + "import": "./src/HostProcess.ts" }, "./testing/longTempDir": { "types": "./src/testing/longTempDir.ts", diff --git a/packages/shared/src/hostProcess.ts b/packages/shared/src/HostProcess.ts similarity index 53% rename from packages/shared/src/hostProcess.ts rename to packages/shared/src/HostProcess.ts index d729624b7630..f30b83234d69 100644 --- a/packages/shared/src/hostProcess.ts +++ b/packages/shared/src/HostProcess.ts @@ -4,57 +4,51 @@ import * as NodeDns from "node:dns"; import * as NodeOS from "node:os"; import * as NodeSea from "node:sea"; -export const HostProcessPlatform = Context.Reference( - "@t3tools/shared/hostProcess/HostProcessPlatform", - { - defaultValue: () => process.platform, - }, -); +export const Platform = Context.Reference("@t3tools/shared/HostProcess/Platform", { + defaultValue: () => process.platform, +}); -export const HostProcessArchitecture = Context.Reference( - "@t3tools/shared/hostProcess/HostProcessArchitecture", +export const Architecture = Context.Reference( + "@t3tools/shared/HostProcess/Architecture", { defaultValue: () => process.arch, }, ); -export const HostProcessHostname = Context.Reference( - "@t3tools/shared/hostProcess/HostProcessHostname", - { - defaultValue: () => NodeOS.hostname(), - }, -); +export const Hostname = Context.Reference("@t3tools/shared/HostProcess/Hostname", { + defaultValue: () => NodeOS.hostname(), +}); -export const HostProcessHomeDirectory = Context.Reference( - "@t3tools/shared/hostProcess/HostProcessHomeDirectory", +export const HomeDirectory = Context.Reference( + "@t3tools/shared/HostProcess/HomeDirectory", { defaultValue: () => NodeOS.homedir(), }, ); -export const HostProcessEnvironment = Context.Reference( - "@t3tools/shared/hostProcess/HostProcessEnvironment", +export const Environment = Context.Reference( + "@t3tools/shared/HostProcess/Environment", { defaultValue: () => process.env, }, ); -export const HostProcessWorkingDirectory = Context.Reference( - "@t3tools/shared/hostProcess/HostProcessWorkingDirectory", +export const WorkingDirectory = Context.Reference( + "@t3tools/shared/HostProcess/WorkingDirectory", { defaultValue: () => process.cwd(), }, ); -export const HostProcessExecutablePath = Context.Reference( - "@t3tools/shared/hostProcess/HostProcessExecutablePath", +export const ExecutablePath = Context.Reference( + "@t3tools/shared/HostProcess/ExecutablePath", { defaultValue: () => process.execPath, }, ); -export const HostProcessArguments = Context.Reference>( - "@t3tools/shared/hostProcess/HostProcessArguments", +export const Arguments = Context.Reference>( + "@t3tools/shared/HostProcess/Arguments", { defaultValue: () => process.argv, }, @@ -65,12 +59,9 @@ export const HostProcessArguments = Context.Reference>( * `t3` for a PATH lookup, `./t3` or the launcher symlink for an explicit * path. `process.argv[0]` and `execPath` are always the resolved binary. */ -export const HostProcessInvokedAs = Context.Reference( - "@t3tools/shared/hostProcess/HostProcessInvokedAs", - { - defaultValue: () => process.argv0, - }, -); +export const InvokedAs = Context.Reference("@t3tools/shared/HostProcess/InvokedAs", { + defaultValue: () => process.argv0, +}); /** * Whether this process is a Node single-executable rather than a script run @@ -78,12 +69,9 @@ export const HostProcessInvokedAs = Context.Reference( * one branches on this: an executable hosts such things as hidden * subcommands of itself. */ -export const HostProcessIsExecutable = Context.Reference( - "@t3tools/shared/hostProcess/HostProcessIsExecutable", - { - defaultValue: () => NodeSea.isSea(), - }, -); +export const IsExecutable = Context.Reference("@t3tools/shared/HostProcess/IsExecutable", { + defaultValue: () => NodeSea.isSea(), +}); /** * Every IP address this machine answers to: the interface addresses, plus @@ -95,8 +83,8 @@ export const HostProcessIsExecutable = Context.Reference( * * Best effort: a failed lookup just leaves the interface set. */ -export const HostProcessAddresses = Context.Reference>>( - "@t3tools/shared/hostProcess/HostProcessAddresses", +export const Addresses = Context.Reference>>( + "@t3tools/shared/HostProcess/Addresses", { defaultValue: () => Effect.gen(function* () { @@ -115,11 +103,8 @@ export const HostProcessAddresses = Context.Reference( - "@t3tools/shared/hostProcess/HostProcessUserId", - { - defaultValue: () => process.getuid?.(), - }, -); +export const UserId = Context.Reference("@t3tools/shared/HostProcess/UserId", { + defaultValue: () => process.getuid?.(), +}); -export const isHostWindows = Effect.map(HostProcessPlatform, (platform) => platform === "win32"); +export const isWindows = Effect.map(Platform, (platform) => platform === "win32"); diff --git a/packages/shared/src/editor.ts b/packages/shared/src/editor.ts index b996b6d12c14..3017e5bb8a30 100644 --- a/packages/shared/src/editor.ts +++ b/packages/shared/src/editor.ts @@ -4,7 +4,7 @@ import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; -import * as HostProcess from "./hostProcess.ts"; +import * as HostProcess from "./HostProcess.ts"; import { isCommandAvailable } from "./shell.ts"; type Editor = (typeof EDITORS)[number]; @@ -37,7 +37,7 @@ export const resolveEditorCommand = Effect.fn("editor.resolveEditorCommand")(fun if (yield* isCommandAvailable(command, { env })) return Option.some({ command, baseArgs }); } - const platform = yield* HostProcess.HostProcessPlatform; + const platform = yield* HostProcess.Platform; const path = yield* Path.Path; const fs = yield* FileSystem.FileSystem; const home = env.HOME; diff --git a/packages/shared/src/logging.test.ts b/packages/shared/src/logging.test.ts index 0e294bbe29bf..7317cb4f7b49 100644 --- a/packages/shared/src/logging.test.ts +++ b/packages/shared/src/logging.test.ts @@ -3,7 +3,7 @@ import * as NodeFS from "node:fs"; import * as NodeOS from "node:os"; import * as NodePath from "node:path"; import { afterEach, describe, expect, it } from "vite-plus/test"; -import { HostProcessPlatform } from "./hostProcess.ts"; +import * as HostProcess from "./HostProcess.ts"; import { RotatingFileSink, @@ -11,7 +11,7 @@ import { RotatingFileSinkError, } from "./logging.ts"; -const windowsHost = HostProcessPlatform.defaultValue() === "win32"; +const windowsHost = HostProcess.Platform.defaultValue() === "win32"; const tempDirectories: string[] = []; const makeTempDirectory = (): string => { diff --git a/packages/shared/src/nodeRuntime.test.ts b/packages/shared/src/nodeRuntime.test.ts index 31c73ac732f6..c298ad405b2e 100644 --- a/packages/shared/src/nodeRuntime.test.ts +++ b/packages/shared/src/nodeRuntime.test.ts @@ -5,12 +5,7 @@ import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Result from "effect/Result"; -import { - HostProcessArguments, - HostProcessExecutablePath, - HostProcessIsExecutable, - HostProcessPlatform, -} from "./hostProcess.ts"; +import * as HostProcess from "./HostProcess.ts"; import { resolveNodeExecutable, resolveSelfInvocation, selfInvocationArgs } from "./nodeRuntime.ts"; import { symlinksSupported } from "./testing/symlinks.ts"; @@ -19,9 +14,9 @@ describe("Self invocation", () => { Effect.gen(function* () { const path = yield* Path.Path; const invocation = yield* resolveSelfInvocation().pipe( - Effect.provideService(HostProcessExecutablePath, "/runtime/node"), - Effect.provideService(HostProcessIsExecutable, false), - Effect.provideService(HostProcessArguments, ["/runtime/node", "dist/bin.mjs", "serve"]), + Effect.provideService(HostProcess.ExecutablePath, "/runtime/node"), + Effect.provideService(HostProcess.IsExecutable, false), + Effect.provideService(HostProcess.Arguments, ["/runtime/node", "dist/bin.mjs", "serve"]), ); expect(invocation.command).toBe("/runtime/node"); expect(invocation.entrypoint).toBe(path.resolve("dist/bin.mjs")); @@ -36,9 +31,9 @@ describe("Self invocation", () => { Effect.gen(function* () { // Node repeats the binary at argv[1] for a single-executable; it is not a script. const invocation = yield* resolveSelfInvocation().pipe( - Effect.provideService(HostProcessExecutablePath, "/packaged/t3"), - Effect.provideService(HostProcessIsExecutable, true), - Effect.provideService(HostProcessArguments, ["/packaged/t3", "/packaged/t3", "serve"]), + Effect.provideService(HostProcess.ExecutablePath, "/packaged/t3"), + Effect.provideService(HostProcess.IsExecutable, true), + Effect.provideService(HostProcess.Arguments, ["/packaged/t3", "/packaged/t3", "serve"]), ); expect(invocation).toEqual({ command: "/packaged/t3", entrypoint: undefined }); expect(selfInvocationArgs(invocation, ["acp-mcp-bridge"])).toEqual(["acp-mcp-bridge"]); @@ -52,8 +47,8 @@ describe("Node runtime selection", () => { for (const executable of ["/runtime/node", "/Applications/T3 Code.app/Electron"]) { expect( yield* resolveNodeExecutable("Local device support", { PATH: "" }).pipe( - Effect.provideService(HostProcessExecutablePath, executable), - Effect.provideService(HostProcessIsExecutable, false), + Effect.provideService(HostProcess.ExecutablePath, executable), + Effect.provideService(HostProcess.IsExecutable, false), ), ).toBe(executable); } @@ -69,8 +64,8 @@ describe("Node runtime selection", () => { }), ).toBe(process.execPath); }).pipe( - Effect.provideService(HostProcessExecutablePath, "/packaged/t3"), - Effect.provideService(HostProcessIsExecutable, true), + Effect.provideService(HostProcess.ExecutablePath, "/packaged/t3"), + Effect.provideService(HostProcess.IsExecutable, true), Effect.provide(NodeServices.layer), ), ); @@ -84,7 +79,7 @@ describe("Node runtime selection", () => { expect(error.message).toContain("Local device support requires Node.js"); expect(error.message).toContain("Install Node.js"); }).pipe( - Effect.provideService(HostProcessIsExecutable, true), + Effect.provideService(HostProcess.IsExecutable, true), Effect.provide(NodeServices.layer), ), ); @@ -94,7 +89,7 @@ describe("Node runtime selection", () => { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const directory = yield* fs.makeTempDirectoryScoped(); - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const node = path.join(directory, platform === "win32" ? "node.exe" : "node"); const env = { PATH: directory }; expect( @@ -107,8 +102,8 @@ describe("Node runtime selection", () => { expect(yield* resolveNodeExecutable("Local device support", env)).toBe(node); }).pipe( Effect.scoped, - Effect.provideService(HostProcessExecutablePath, "/packaged/t3"), - Effect.provideService(HostProcessIsExecutable, true), + Effect.provideService(HostProcess.ExecutablePath, "/packaged/t3"), + Effect.provideService(HostProcess.IsExecutable, true), Effect.provide(NodeServices.layer), ), ); @@ -134,9 +129,9 @@ describe("Node runtime selection", () => { ).toBe(node); }).pipe( Effect.scoped, - Effect.provideService(HostProcessExecutablePath, "/packaged/t3"), - Effect.provideService(HostProcessIsExecutable, true), - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.ExecutablePath, "/packaged/t3"), + Effect.provideService(HostProcess.IsExecutable, true), + Effect.provideService(HostProcess.Platform, "win32"), Effect.provide(NodeServices.layer), ), ); @@ -156,8 +151,8 @@ describe("Node runtime selection", () => { expect(error.message).toContain("Install Node.js"); }).pipe( Effect.scoped, - Effect.provideService(HostProcessIsExecutable, true), - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.IsExecutable, true), + Effect.provideService(HostProcess.Platform, "win32"), Effect.provide(NodeServices.layer), ), ); @@ -167,21 +162,21 @@ describe("Node runtime selection", () => { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const directory = yield* fs.makeTempDirectoryScoped(); - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const executable = path.join(directory, platform === "win32" ? "t3.exe" : "t3"); const node = path.join(directory, platform === "win32" ? "node.exe" : "node"); yield* fs.writeFileString(executable, "standalone executable fixture"); yield* fs.chmod(executable, 0o755); yield* fs.link(executable, node); const error = yield* resolveNodeExecutable("Local device support", { PATH: directory }).pipe( - Effect.provideService(HostProcessExecutablePath, executable), + Effect.provideService(HostProcess.ExecutablePath, executable), Effect.flip, ); expect(error._tag).toBe("NodeRuntimeUnavailableError"); expect(error.message).toContain("Install Node.js"); }).pipe( Effect.scoped, - Effect.provideService(HostProcessIsExecutable, true), + Effect.provideService(HostProcess.IsExecutable, true), Effect.provide(NodeServices.layer), ), ); @@ -191,14 +186,14 @@ describe("Node runtime selection", () => { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const directory = yield* fs.makeTempDirectoryScoped(); - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const node = path.join(directory, platform === "win32" ? "node.exe" : "node"); yield* fs.symlink(process.execPath, node); expect(yield* resolveNodeExecutable("Local device support", { PATH: directory })).toBe(node); }).pipe( Effect.scoped, - Effect.provideService(HostProcessExecutablePath, "/packaged/t3"), - Effect.provideService(HostProcessIsExecutable, true), + Effect.provideService(HostProcess.ExecutablePath, "/packaged/t3"), + Effect.provideService(HostProcess.IsExecutable, true), Effect.provide(NodeServices.layer), ), ); @@ -210,7 +205,7 @@ describe("Node runtime selection", () => { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const directory = yield* fs.makeTempDirectoryScoped(); - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const node = path.join(directory, platform === "win32" ? "node.exe" : "node"); yield* fs.symlink(process.execPath, node); const error = yield* resolveNodeExecutable("Local device support", { @@ -219,7 +214,7 @@ describe("Node runtime selection", () => { expect(error.message).toContain("Install Node.js"); }).pipe( Effect.scoped, - Effect.provideService(HostProcessIsExecutable, true), + Effect.provideService(HostProcess.IsExecutable, true), Effect.provide(NodeServices.layer), ), ); diff --git a/packages/shared/src/nodeRuntime.ts b/packages/shared/src/nodeRuntime.ts index baa4d9dc610c..4ad752f43d0d 100644 --- a/packages/shared/src/nodeRuntime.ts +++ b/packages/shared/src/nodeRuntime.ts @@ -4,13 +4,7 @@ import * as Path from "effect/Path"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; -import { - HostProcessArguments, - HostProcessEnvironment, - HostProcessExecutablePath, - HostProcessIsExecutable, - HostProcessPlatform, -} from "./hostProcess.ts"; +import * as HostProcess from "./HostProcess.ts"; import { CommandResolutionCache, resolveCommandPath } from "./shell.ts"; const NodeRuntimeFeature = Schema.Literals([ @@ -51,11 +45,11 @@ export interface SelfInvocation { * assemble the pair themselves. */ export const resolveSelfInvocation = Effect.fn("nodeRuntime.resolveSelfInvocation")(function* () { - const command = yield* HostProcessExecutablePath; - if (yield* HostProcessIsExecutable) + const command = yield* HostProcess.ExecutablePath; + if (yield* HostProcess.IsExecutable) return { command, entrypoint: undefined } satisfies SelfInvocation; const path = yield* Path.Path; - const entry = (yield* HostProcessArguments)[1]; + const entry = (yield* HostProcess.Arguments)[1]; // Children spawn from their own working directory, so the script path must be absolute. return { command, @@ -75,13 +69,13 @@ export const resolveNodeExecutable = Effect.fn("nodeRuntime.resolveNodeExecutabl feature: typeof NodeRuntimeFeature.Type, environment?: NodeJS.ProcessEnv, ) { - const executablePath = yield* HostProcessExecutablePath; - if (!(yield* HostProcessIsExecutable)) return executablePath; + const executablePath = yield* HostProcess.ExecutablePath; + if (!(yield* HostProcess.IsExecutable)) return executablePath; const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const platform = yield* HostProcessPlatform; - const env = environment ?? (yield* HostProcessEnvironment); + const platform = yield* HostProcess.Platform; + const env = environment ?? (yield* HostProcess.Environment); const nodePath = yield* resolveCommandPath(platform === "win32" ? "node.exe" : "node", { // Batch wrappers require a shell; helper callers launch the runtime directly. env: platform === "win32" ? { ...env, PATHEXT: ".EXE" } : env, diff --git a/packages/shared/src/relayClient.test.ts b/packages/shared/src/relayClient.test.ts index a96f3f8d864b..d743067b6209 100644 --- a/packages/shared/src/relayClient.test.ts +++ b/packages/shared/src/relayClient.test.ts @@ -14,18 +14,18 @@ import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; import { HttpClient, HttpClientResponse } from "effect/http"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; -import { HostProcessArchitecture, HostProcessPlatform } from "./hostProcess.ts"; +import * as HostProcess from "./HostProcess.ts"; import * as RelayClient from "./relayClient.ts"; // The suite runs the linux code path against the real filesystem, checking // POSIX exec bits that NTFS never reports; the win32 branch skips that check. -const windowsHost = HostProcessPlatform.defaultValue() === "win32"; +const windowsHost = HostProcess.Platform.defaultValue() === "win32"; const layerHostRuntime = (env: Record = {}, platform: NodeJS.Platform = "linux") => Layer.mergeAll( - Layer.succeed(HostProcessPlatform, platform), - Layer.succeed(HostProcessArchitecture, "x64"), + Layer.succeed(HostProcess.Platform, platform), + Layer.succeed(HostProcess.Architecture, "x64"), ConfigProvider.layer(ConfigProvider.fromEnv({ env })), ); diff --git a/packages/shared/src/relayClient.ts b/packages/shared/src/relayClient.ts index 3705e7677cdd..44a2429d4231 100644 --- a/packages/shared/src/relayClient.ts +++ b/packages/shared/src/relayClient.ts @@ -21,7 +21,7 @@ import * as Schedule from "effect/Schedule"; import * as Semaphore from "effect/Semaphore"; import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; -import { HostProcessArchitecture, HostProcessPlatform } from "./hostProcess.ts"; +import * as HostProcess from "./HostProcess.ts"; export const CLOUDFLARED_VERSION = "2026.10.0"; // The oldest release that accepts every flag the connector is started with @@ -257,8 +257,8 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function const path = yield* Path.Path; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const installSemaphore = yield* Semaphore.make(1); - const platform = yield* HostProcessPlatform; - const arch = yield* HostProcessArchitecture; + const platform = yield* HostProcess.Platform; + const arch = yield* HostProcess.Architecture; const releaseAsset = options.releaseAsset ?? resolveReleaseAsset(platform, arch); const loadCloudflaredConfig = Effect.suspend(() => CloudflaredConfig).pipe(Effect.orDie); const managedRoot = path.join(options.baseDir, "tools", "cloudflared"); diff --git a/packages/shared/src/shell.test.ts b/packages/shared/src/shell.test.ts index 4e27aa49fcfc..428d0d5951f5 100644 --- a/packages/shared/src/shell.test.ts +++ b/packages/shared/src/shell.test.ts @@ -1,6 +1,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { it as effectIt } from "@effect/vitest"; -import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; @@ -342,7 +342,7 @@ effectIt.layer(NodeServices.layer)("isCommandAvailable", (it) => { expect( yield* isCommandAvailable("definitely-not-installed", { env: { PATH: "", PATHEXT: ".COM;.EXE;.BAT;.CMD" }, - }).pipe(Effect.provideService(HostProcessPlatform, "win32")), + }).pipe(Effect.provideService(HostProcess.Platform, "win32")), ).toBe(false); }), ); @@ -353,7 +353,7 @@ effectIt.layer(NodeServices.layer)("resolveCommandPath", (it) => { Effect.gen(function* () { const result = yield* resolveCommandPath("definitely-not-installed", { env: { PATH: "", PATHEXT: ".COM;.EXE;.BAT;.CMD" }, - }).pipe(Effect.provideService(HostProcessPlatform, "win32"), Effect.result); + }).pipe(Effect.provideService(HostProcess.Platform, "win32"), Effect.result); expect(result._tag).toBe("Failure"); }), @@ -367,7 +367,7 @@ effectIt.layer(NodeServices.layer)("resolveCommandPath", (it) => { Effect.gen(function* () { const probed: Array = []; const result = yield* resolveCommandPath("definitely-not-installed", { env }).pipe( - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), Effect.provideService(CommandResolutionCache, new Map()), Effect.provide( FileSystem.layerNoop({ @@ -425,7 +425,7 @@ effectIt.layer(NodeServices.layer)("resolveCommandPath", (it) => { const resolved = yield* resolveCommandPath(command, { env: { PATH: cwd, PATHEXT: ".CMD" }, }).pipe( - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), Effect.provideService(CommandResolutionCache, new Map()), Effect.provideService(FileSystem.FileSystem, { ...fs, @@ -458,7 +458,7 @@ effectIt.layer(NodeServices.layer)("resolveCommandPath", (it) => { yield* TestClock.adjust("30 seconds"); expect(yield* resolveCommandPath("appeared", options)).toBe(executable); }).pipe( - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), Effect.provideService(CommandResolutionCache, new Map()), ), ); @@ -505,7 +505,7 @@ effectIt.layer(NodeServices.layer)("resolveCommandPath", (it) => { ); expect(probed).toEqual([path.join(first, "cursor.CMD"), path.join(second, "late.EXE")]); }).pipe( - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), Effect.provideService(CommandResolutionCache, new Map()), ), ); @@ -517,7 +517,7 @@ effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => { const command = yield* resolveSpawnCommand("node.exe", ["script.js", "hello & goodbye"], { env: { PATH: "", PATHEXT: ".COM;.EXE;.BAT;.CMD" }, }).pipe( - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), Effect.provideService(CommandResolutionCache, new Map()), ); @@ -536,7 +536,7 @@ effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => { ["run", "value & calc", "%PATH%", 'quote"value'], { env: { PATH: "", PATHEXT: ".COM;.EXE;.BAT;.CMD" } }, ).pipe( - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), Effect.provideService(CommandResolutionCache, new Map()), Effect.provideService( SpawnExecutableResolution, @@ -563,9 +563,9 @@ effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => { env: { CODEX_HOME: "C:\\Users\\tester\\.codex" }, extendEnv: true, }).pipe( - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), Effect.provideService(CommandResolutionCache, new Map()), - Effect.provideService(HostProcessEnvironment, { + Effect.provideService(HostProcess.Environment, { PATH: "C:\\Users\\tester\\AppData\\Roaming\\npm", PATHEXT: ".COM;.EXE;.BAT;.CMD", }), @@ -592,7 +592,7 @@ effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => { }; const resolve = (command: string, path: string, resolver = scan) => resolveSpawnCommand(command, [], { env: { PATH: path, PATHEXT: ".EXE" } }).pipe( - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), Effect.provideService(SpawnExecutableResolution, resolver), ); @@ -632,7 +632,7 @@ effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => { const command = yield* resolveSpawnCommand("missing & calc", ["unsafe & value"], { env: { PATH: "", PATHEXT: ".COM;.EXE;.BAT;.CMD" }, }).pipe( - Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcess.Platform, "win32"), Effect.provideService(CommandResolutionCache, new Map()), ); diff --git a/packages/shared/src/shell.ts b/packages/shared/src/shell.ts index 1742aa33065b..b24c998b1785 100644 --- a/packages/shared/src/shell.ts +++ b/packages/shared/src/shell.ts @@ -11,7 +11,7 @@ import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; -import { HostProcessEnvironment, HostProcessPlatform } from "./hostProcess.ts"; +import * as HostProcess from "./HostProcess.ts"; import * as Context from "effect/Context"; const SHELL_ENV_NAME_PATTERN = /^[A-Z0-9_]+$/; const WINDOWS_PATH_DELIMITER = ";"; @@ -503,7 +503,7 @@ interface CommandResolutionCacheEntry { readonly expiresAtNanos: bigint; } -// The cache lives in the Effect environment (like HostProcessPlatform above) +// The cache lives in the Effect environment (like HostProcess.Platform above) // so tests and embedders can provide an isolated instance; the default is a // single process-wide map shared by all consumers. export const CommandResolutionCache = Context.Reference>( @@ -688,8 +688,8 @@ export const resolveCommandPath = Effect.fn("shell.resolveCommandPath")(function options: CommandAvailabilityOptions = {}, ) { return yield* resolveCommandPathForPlatform(command, { - env: options.env ?? (yield* HostProcessEnvironment), - platform: yield* HostProcessPlatform, + env: options.env ?? (yield* HostProcess.Environment), + platform: yield* HostProcess.Platform, }); }); @@ -699,12 +699,12 @@ export const resolveSpawnCommand = Effect.fnUntraced(function* ( args: ReadonlyArray, options: CommandAvailabilityOptions = {}, ): Effect.fn.Return { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; if (platform !== "win32") { return { command, args: [...args], shell: false }; } - const hostEnvironment = yield* HostProcessEnvironment; + const hostEnvironment = yield* HostProcess.Environment; const env = options.env === undefined ? hostEnvironment diff --git a/packages/shared/src/testing/longTempDir.ts b/packages/shared/src/testing/longTempDir.ts index 8a18abb8ce42..669d6da36971 100644 --- a/packages/shared/src/testing/longTempDir.ts +++ b/packages/shared/src/testing/longTempDir.ts @@ -1,14 +1,14 @@ // @effect-diagnostics nodeBuiltinImport:off - runs once at test setup, outside any Effect runtime. import * as NodeFS from "node:fs"; import * as NodeOS from "node:os"; -import { HostProcessPlatform } from "../hostProcess.ts"; +import * as HostProcess from "../HostProcess.ts"; // GitHub's Windows runners hand out the temp directory by its 8.3 short name // (C:\Users\RUNNER~1\...). Anything that canonicalises a path, such as git or // realpath, reports the long form, so equality checks between a temp path and // its canonical form fail. Node reads TEMP/TMP on every os.tmpdir() call, so // pointing them at the long form fixes every temp directory the suite makes. -if (HostProcessPlatform.defaultValue() === "win32") { +if (HostProcess.Platform.defaultValue() === "win32") { try { const longForm = NodeFS.realpathSync.native(NodeOS.tmpdir()); process.env.TEMP = longForm; diff --git a/packages/source-control-forgejo/src/server/ForgejoCli.ts b/packages/source-control-forgejo/src/server/ForgejoCli.ts index 46afc4522b45..90501a4591ca 100644 --- a/packages/source-control-forgejo/src/server/ForgejoCli.ts +++ b/packages/source-control-forgejo/src/server/ForgejoCli.ts @@ -11,7 +11,7 @@ import * as NodePath from "node:path"; import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http"; import { ChildProcessSpawner } from "effect/process"; import { decodeJsonResult } from "@t3tools/shared/schemaJson"; -import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; import { collectUint8StreamText } from "@t3tools/provider-core/server/collectStreamText"; import type { SourceControlProviderContext } from "@t3tools/source-control-core/server/SourceControlProvider"; @@ -248,8 +248,8 @@ export const make = Effect.gen(function* () { const readKeys = Effect.fn("ForgejoCli.readKeys")(function* (cwd: string) { for (const path of forgejoKeysPaths({ - platform: yield* HostProcessPlatform, - home: yield* HostProcessHomeDirectory, + platform: yield* HostProcess.Platform, + home: yield* HostProcess.HomeDirectory, ...(globalThis.process.env.XDG_DATA_HOME ? { dataHome: globalThis.process.env.XDG_DATA_HOME } : {}), diff --git a/packages/source-control-github/src/server/GitHubCredentials.ts b/packages/source-control-github/src/server/GitHubCredentials.ts index 7483d7e02a52..9eaa52301c04 100644 --- a/packages/source-control-github/src/server/GitHubCredentials.ts +++ b/packages/source-control-github/src/server/GitHubCredentials.ts @@ -11,7 +11,7 @@ import * as PlatformError from "effect/PlatformError"; import * as Redacted from "effect/Redacted"; import * as Schema from "effect/Schema"; -import { HostProcessEnvironment, HostProcessWorkingDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; @@ -130,8 +130,8 @@ export const make = Effect.gen(function* () { const sourceControlHost = yield* SourceControlHost.SourceControlHost; const process = sourceControlHost.process; const crypto = yield* Crypto.Crypto; - const environment = yield* HostProcessEnvironment; - const workingDirectory = yield* HostProcessWorkingDirectory; + const environment = yield* HostProcess.Environment; + const workingDirectory = yield* HostProcess.WorkingDirectory; /** `host:sha256(token)`, safe for cache keys and rate-limit scopes. */ const fingerprintOf = (host: string, token: string) => diff --git a/packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts b/packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts index fee5d5e9546c..3f17db9bb302 100644 --- a/packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts +++ b/packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts @@ -1,4 +1,4 @@ -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { assert, describe, it } from "@effect/vitest"; import * as NodeServices from "@effect/platform-node/NodeServices"; import * as Effect from "effect/Effect"; @@ -1075,7 +1075,7 @@ it.live.each([ remoteName: "enterprise", remoteBranch: "feature", }); - }).pipe(Effect.provide(layer), Effect.provideService(HostProcessEnvironment, {}), Effect.scoped); + }).pipe(Effect.provide(layer), Effect.provideService(HostProcess.Environment, {}), Effect.scoped); }); it.effect.each([ @@ -1162,7 +1162,7 @@ it.effect.each([ ]); }).pipe( Effect.provide(layer), - Effect.provideService(HostProcessEnvironment, { GH_REPO: envRepository }), + Effect.provideService(HostProcess.Environment, { GH_REPO: envRepository }), Effect.scoped, ); }, diff --git a/packages/source-control-github/src/server/GitHubSourceControlProvider.ts b/packages/source-control-github/src/server/GitHubSourceControlProvider.ts index 8edf9423c7c9..0e54af539d9f 100644 --- a/packages/source-control-github/src/server/GitHubSourceControlProvider.ts +++ b/packages/source-control-github/src/server/GitHubSourceControlProvider.ts @@ -18,7 +18,7 @@ import { type SourceControlRepositoryCloneUrls, } from "@t3tools/contracts"; import { normalizeGitRemoteUrl } from "@t3tools/shared/git"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { decodeJsonResult } from "@t3tools/shared/schemaJson"; import { isSshRemoteUrl } from "@t3tools/shared/sourceControl"; @@ -193,7 +193,7 @@ export const makeDiscovery = Effect.gen(function* () { const api = yield* GitHubApi.GitHubApi; const sourceControlHost = yield* SourceControlHost.SourceControlHost; const process = sourceControlHost.process; - const environment = yield* HostProcessEnvironment; + const environment = yield* HostProcess.Environment; return { type: "managed-cli", @@ -478,7 +478,7 @@ export const make = Effect.gen(function* () { const api = yield* GitHubApi.GitHubApi; const sourceControlHost = yield* SourceControlHost.SourceControlHost; const process = sourceControlHost.process; - const environment = yield* HostProcessEnvironment; + const environment = yield* HostProcess.Environment; const git = sourceControlHost.git; const fileSystem = yield* FileSystem.FileSystem; diff --git a/packages/ssh/src/auth.test.ts b/packages/ssh/src/auth.test.ts index 3cd42ad43825..2c507db668e7 100644 --- a/packages/ssh/src/auth.test.ts +++ b/packages/ssh/src/auth.test.ts @@ -5,7 +5,7 @@ import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { buildSshAskpassHelperDescriptor, @@ -50,7 +50,7 @@ describe("ssh auth", () => { assert.equal(yield* fs.exists(askpassPath), true); assert.include(yield* fs.readFileString(askpassPath), 'printf "%s\\n" "$T3_SSH_AUTH_SECRET"'); }).pipe( - Effect.provide(Layer.merge(NodeServices.layer, Layer.succeed(HostProcessPlatform, "linux"))), + Effect.provide(Layer.merge(NodeServices.layer, Layer.succeed(HostProcess.Platform, "linux"))), Effect.scoped, ), ); @@ -61,7 +61,7 @@ describe("ssh auth", () => { directory: "C:\\temp\\t3code-ssh-askpass", }).pipe( Effect.provide( - Layer.merge(NodeServices.layer, Layer.succeed(HostProcessPlatform, "win32")), + Layer.merge(NodeServices.layer, Layer.succeed(HostProcess.Platform, "win32")), ), ); diff --git a/packages/ssh/src/auth.ts b/packages/ssh/src/auth.ts index fd4ec9dbc01e..a8bf22bf7e14 100644 --- a/packages/ssh/src/auth.ts +++ b/packages/ssh/src/auth.ts @@ -1,4 +1,4 @@ -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Config from "effect/Config"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; @@ -113,7 +113,7 @@ export const buildSshAskpassHelperDescriptor = Effect.fn( )(function* (input: { readonly directory: string; }): Effect.fn.Return { - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const path = yield* Path.Path; const directory = input.directory; @@ -152,7 +152,7 @@ const ensureSshAskpassHelpers = Effect.fn("ssh/auth.ensureSshAskpassHelpers")(fu const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const descriptor = yield* buildSshAskpassHelperDescriptor(input); - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; yield* fs.makeDirectory(path.dirname(descriptor.launcherPath), { recursive: true }); @@ -182,7 +182,7 @@ export const buildSshChildEnvironment = Effect.fn("ssh/auth.buildSshChildEnviron return baseEnv; } - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const hostDisplay = input.baseEnv ? input.baseEnv.DISPLAY : yield* Config.String("DISPLAY").pipe( diff --git a/packages/ssh/src/command.ts b/packages/ssh/src/command.ts index 8c9e2d304618..b4b42598e382 100644 --- a/packages/ssh/src/command.ts +++ b/packages/ssh/src/command.ts @@ -1,5 +1,5 @@ import type { DesktopSshEnvironmentTarget } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Crypto from "effect/Crypto"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; @@ -25,7 +25,7 @@ const MAX_SSH_ERROR_OUTPUT_LENGTH = 4_000; const sshCommandForPlatform = (platform: NodeJS.Platform): string => platform === "win32" ? "ssh.exe" : "ssh"; -export const resolveSshCommand = Effect.map(HostProcessPlatform, sshCommandForPlatform); +export const resolveSshCommand = Effect.map(HostProcess.Platform, sshCommandForPlatform); const encoder = new TextEncoder(); diff --git a/packages/ssh/src/runnerProcess.test.ts b/packages/ssh/src/runnerProcess.test.ts index bc6064628843..78bac0aac93f 100644 --- a/packages/ssh/src/runnerProcess.test.ts +++ b/packages/ssh/src/runnerProcess.test.ts @@ -1,6 +1,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; @@ -21,7 +21,7 @@ const Started = Schema.Struct({ }); const decodeStarted = Schema.decodeUnknownSync(Schema.fromJsonString(Started)); -describe.skipIf(HostProcessPlatform.defaultValue() === "win32")( +describe.skipIf(HostProcess.Platform.defaultValue() === "win32")( "remote runner process ownership", () => { it.live("keeps the server PID and graceful shutdown through the node-script runner", () => @@ -130,7 +130,7 @@ server.listen(Number(process.env.T3_TEST_PORT ?? 0), "127.0.0.1", () => { }, ); -describe.skipIf(HostProcessPlatform.defaultValue() === "win32")( +describe.skipIf(HostProcess.Platform.defaultValue() === "win32")( "remote stop process ownership", () => { it.live.each(["graceful", "timeout", "external"] as const)( diff --git a/packages/ssh/src/tunnel.test.ts b/packages/ssh/src/tunnel.test.ts index d0943d1e382f..f7f46926ddf9 100644 --- a/packages/ssh/src/tunnel.test.ts +++ b/packages/ssh/src/tunnel.test.ts @@ -1,7 +1,7 @@ import { assert, describe, it } from "@effect/vitest"; import * as NodeServices from "@effect/platform-node/NodeServices"; import * as NetService from "@t3tools/shared/Net"; -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Deferred from "effect/Deferred"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; @@ -691,8 +691,8 @@ describe("ssh tunnel scripts", () => { // lock excludes concurrent installers. Run the real script against a tiny // fake archive served from a file:// mirror. describe("archive runner script", () => { - const hostPlatform = HostProcessPlatform.defaultValue(); - const hostArch = HostProcessArchitecture.defaultValue(); + const hostPlatform = HostProcess.Platform.defaultValue(); + const hostArch = HostProcess.Architecture.defaultValue(); const windowsHost = hostPlatform === "win32"; const archiveVersion = "1.2.3-preview.20260911.4"; diff --git a/packages/tailscale/src/tailscale.test.ts b/packages/tailscale/src/tailscale.test.ts index cf297d570107..c800bf72defc 100644 --- a/packages/tailscale/src/tailscale.test.ts +++ b/packages/tailscale/src/tailscale.test.ts @@ -8,7 +8,7 @@ import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; import { ChildProcessSpawner } from "effect/process"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { buildTailscaleHttpsBaseUrl, @@ -104,7 +104,7 @@ function neverFinishingMockHandle() { function layerSpawner(spawner: ChildProcessSpawner.ChildProcessSpawner["Service"]) { return Layer.merge( Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner), - Layer.succeed(HostProcessPlatform, "linux"), + Layer.succeed(HostProcess.Platform, "linux"), ); } diff --git a/packages/tailscale/src/tailscale.ts b/packages/tailscale/src/tailscale.ts index c1da6fe2b2d4..e3579891b4c6 100644 --- a/packages/tailscale/src/tailscale.ts +++ b/packages/tailscale/src/tailscale.ts @@ -1,4 +1,4 @@ -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; @@ -217,7 +217,7 @@ export const parseTailscaleStatus = ( export const readTailscaleStatus = Effect.gen(function* () { const args = ["status", "--json"]; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const hostPlatform = yield* HostProcessPlatform; + const hostPlatform = yield* HostProcess.Platform; const executable = tailscaleCommandForPlatform(hostPlatform); const commandContext = { executable, @@ -291,7 +291,7 @@ const runTailscaleCommand = ( ): Effect.Effect => Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const hostPlatform = yield* HostProcessPlatform; + const hostPlatform = yield* HostProcess.Platform; const executable = tailscaleCommandForPlatform(hostPlatform); const commandContext = { executable, diff --git a/scripts/build-cli-archive.ts b/scripts/build-cli-archive.ts index 65835de83019..85f7ea8da5fe 100644 --- a/scripts/build-cli-archive.ts +++ b/scripts/build-cli-archive.ts @@ -28,7 +28,7 @@ import * as Schema from "effect/Schema"; import { Command, Flag } from "effect/cli"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { fromYaml } from "@t3tools/shared/schemaYaml"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; import rootPackageJson from "../package.json" with { type: "json" }; @@ -478,8 +478,8 @@ const buildCliArchive = Effect.fn("buildCliArchive")(function* (input: { ); // The unsuffixed host build is only a valid stand-in when it was built for // this platform and architecture; otherwise a missing target must fail. - const hostPlatform = yield* HostProcessPlatform; - const hostKey = `${hostPlatform === "win32" ? "win" : hostPlatform}-${yield* HostProcessArchitecture}`; + const hostPlatform = yield* HostProcess.Platform; + const hostKey = `${hostPlatform === "win32" ? "win" : hostPlatform}-${yield* HostProcess.Architecture}`; const builtExecutable = (yield* fs.exists(targetExecutable)) ? targetExecutable : targetKey === hostKey diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index 2fc9adf94526..8d27a55266fb 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -93,7 +93,7 @@ import { wslRuntimeArchiveStem, } from "./build-desktop-artifact.ts"; import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts"; -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; // Keeps pnpm from auto-installing TypeScript, a types-only peer, into the app. @@ -1263,7 +1263,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { assert.isBelow(result.fileCount, WINDOWS_PACKAGED_PAYLOAD_FILE_LIMIT); assert.deepStrictEqual(secondAsar, firstAsar); }), - ).pipe(Effect.provideService(HostProcessPlatform, "linux")), + ).pipe(Effect.provideService(HostProcess.Platform, "linux")), ); it.effect("accepts an embedded Linux CLI release archive with a matching digest", () => @@ -1283,7 +1283,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { assert.equal(result.packagedAppDir, fixture.packagedAppDir); }), - ).pipe(Effect.provideService(HostProcessPlatform, "linux")), + ).pipe(Effect.provideService(HostProcess.Platform, "linux")), ); it.effect.each(["x64", "arm64"] as const)( @@ -1307,7 +1307,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { assert.equal(result.packagedAppDir, fixture.packagedAppDir); }), - ).pipe(Effect.provideService(HostProcessPlatform, "linux")), + ).pipe(Effect.provideService(HostProcess.Platform, "linux")), ); it.effect.each(["x64", "arm64"] as const)( @@ -1498,8 +1498,8 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { Effect.provide( Layer.mergeAll( spawnerLayer, - Layer.succeed(HostProcessPlatform, "win32"), - Layer.succeed(HostProcessArchitecture, "x64"), + Layer.succeed(HostProcess.Platform, "win32"), + Layer.succeed(HostProcess.Architecture, "x64"), ), ), ); @@ -1541,8 +1541,8 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { Effect.provide( Layer.mergeAll( spawnerLayer, - Layer.succeed(HostProcessPlatform, "linux"), - Layer.succeed(HostProcessArchitecture, "x64"), + Layer.succeed(HostProcess.Platform, "linux"), + Layer.succeed(HostProcess.Architecture, "x64"), ), ), ); @@ -1577,8 +1577,8 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { Effect.provide( Layer.mergeAll( spawnerLayer, - Layer.succeed(HostProcessPlatform, "darwin"), - Layer.succeed(HostProcessArchitecture, "arm64"), + Layer.succeed(HostProcess.Platform, "darwin"), + Layer.succeed(HostProcess.Architecture, "arm64"), ), ), ); @@ -1623,8 +1623,8 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { Effect.provide( Layer.mergeAll( spawnerLayer, - Layer.succeed(HostProcessPlatform, "win32"), - Layer.succeed(HostProcessArchitecture, "x64"), + Layer.succeed(HostProcess.Platform, "win32"), + Layer.succeed(HostProcess.Architecture, "x64"), ), ), ); @@ -1652,8 +1652,8 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { ).pipe( Effect.provide( Layer.mergeAll( - Layer.succeed(HostProcessPlatform, "win32"), - Layer.succeed(HostProcessArchitecture, "x64"), + Layer.succeed(HostProcess.Platform, "win32"), + Layer.succeed(HostProcess.Architecture, "x64"), ), ), ), @@ -1764,7 +1764,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { assert.instanceOf(error, BundleNotSelfContainedError); assert.include(error.output, "t3code-deliberately-missing-package"); }), - ).pipe(Effect.provideService(HostProcessPlatform, "linux")), + ).pipe(Effect.provideService(HostProcess.Platform, "linux")), ); it.effect("preserves both Linux icon resize failures with structural context", () => { @@ -2333,8 +2333,8 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { }).pipe( Effect.provide( Layer.mergeAll( - Layer.succeed(HostProcessPlatform, "win32"), - Layer.succeed(HostProcessArchitecture, "x64"), + Layer.succeed(HostProcess.Platform, "win32"), + Layer.succeed(HostProcess.Architecture, "x64"), ConfigProvider.layer( ConfigProvider.fromEnv({ env: { diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index ed14f57b61a4..374e37830662 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -16,7 +16,7 @@ import { } from "@electron/asar"; import { fromYaml } from "@t3tools/shared/schemaYaml"; -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { clerkFrontendApiHostnameFromPublishableKey } from "@t3tools/shared/relayAuth"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; import rootPackageJson from "../package.json" with { type: "json" }; @@ -869,7 +869,7 @@ const resolveGitCommitHash = Effect.fn("resolveGitCommitHash")(function* (repoRo const resolvePythonForNodeGyp = Effect.fn("resolvePythonForNodeGyp")(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const hostPlatform = yield* HostProcessPlatform; + const hostPlatform = yield* HostProcess.Platform; const env = yield* Config.all({ configuredPython: Config.String("npm_config_python").pipe( Config.orElse(() => Config.String("PYTHON")), @@ -1716,7 +1716,7 @@ export const resolveBuildOptions = Effect.fn("resolveBuildOptions")(function* ( const path = yield* Path.Path; const repoRoot = yield* RepoRoot; const env = yield* BuildEnvConfig; - const hostPlatform = yield* HostProcessPlatform; + const hostPlatform = yield* HostProcess.Platform; const platform = mergeOptions( input.platform, @@ -2404,7 +2404,7 @@ export const stageBrowserSecret = Effect.fn("stageBrowserSecret")(function* (inp // silently. `universal` is a mac-only arch the option type still admits; // the helper script rejects it, so it maps to the concrete x64 the Linux // resource monitor uses for the same request. - const hostPlatform = yield* HostProcessPlatform; + const hostPlatform = yield* HostProcess.Platform; if (hostPlatform !== "linux") { return yield* new LinuxBrowserSecretHostError({ hostPlatform }); } @@ -3165,8 +3165,8 @@ export const verifyWindowsPrimaryFffNativeLoad = Effect.fn( readonly targetArch: typeof BuildArch.Type; readonly verbose: boolean; }) { - const hostPlatform = yield* HostProcessPlatform; - const hostArchitecture = yield* HostProcessArchitecture; + const hostPlatform = yield* HostProcess.Platform; + const hostArchitecture = yield* HostProcess.Architecture; const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const executablePath = path.join(input.packagedAppDir, input.appExecutableName); @@ -3485,7 +3485,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( const repoRoot = yield* RepoRoot; const path = yield* Path.Path; const fs = yield* FileSystem.FileSystem; - const hostPlatform = yield* HostProcessPlatform; + const hostPlatform = yield* HostProcess.Platform; if (hostPlatform === "linux" && options.platform === "linux") { yield* preflightLinuxDesktopBuild(options.arch); } diff --git a/scripts/build-npm-platform-packages.test.ts b/scripts/build-npm-platform-packages.test.ts index cd813ae742b7..6ba620528bfe 100644 --- a/scripts/build-npm-platform-packages.test.ts +++ b/scripts/build-npm-platform-packages.test.ts @@ -1,4 +1,4 @@ -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; @@ -203,8 +203,8 @@ it.layer(NodeServices.layer)("build-npm-platform-packages", (it) => { // NODE_PATH stands in for node_modules: require.resolve finds the // platform package there exactly as it would after `npm install`. - const hostPlatform = yield* HostProcessPlatform; - const hostArch = yield* HostProcessArchitecture; + const hostPlatform = yield* HostProcess.Platform; + const hostArch = yield* HostProcess.Architecture; const env = { ...process.env, NODE_PATH: fixture.outputDir } as Record; if (KEYS.some((key) => key === `${hostPlatform}-${hostArch}`)) { const passthrough = yield* run(process.execPath, ["bin/t3.js", "serve", "--port", "1234"], { diff --git a/scripts/build-npm-platform-packages.ts b/scripts/build-npm-platform-packages.ts index 26ac80b1a3d9..289a6b32522f 100644 --- a/scripts/build-npm-platform-packages.ts +++ b/scripts/build-npm-platform-packages.ts @@ -36,7 +36,7 @@ import { cliArchiveFileName, type CliArchivePlatformKey, } from "@t3tools/shared/cliRelease"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { fromJsonStringPretty } from "@t3tools/shared/schemaJson"; import { isCommandAvailable } from "@t3tools/shared/shell"; import serverPackageJson from "../apps/server/package.json" with { type: "json" }; @@ -253,7 +253,7 @@ const runCommand = Effect.fn("runCommand")(function* ( const extractArchive = Effect.fn("extractArchive")(function* (archive: string, into: string) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; if (!archive.endsWith(".zip")) { yield* runCommand(ChildProcess.make("tar", ["-xf", archive, "-C", into]), "tar -xf"); } else if (platform === "win32") { @@ -282,7 +282,7 @@ const extractArchive = Effect.fn("extractArchive")(function* (archive: string, i }); /** Tar to build npm tarballs with; see build-cli-archive.ts for why Windows names bsdtar by path. */ -const hostTar = Effect.map(HostProcessPlatform, (platform) => +const hostTar = Effect.map(HostProcess.Platform, (platform) => platform === "win32" ? windowsSystemTar() : "tar", ); diff --git a/scripts/dev-runner.test.ts b/scripts/dev-runner.test.ts index c8363c181976..950d0214c491 100644 --- a/scripts/dev-runner.test.ts +++ b/scripts/dev-runner.test.ts @@ -4,11 +4,7 @@ import * as NodeFS from "node:fs"; import * as NodeOS from "node:os"; import * as NodePath from "node:path"; import * as NetService from "@t3tools/shared/Net"; -import { - HostProcessEnvironment, - HostProcessPlatform, - HostProcessWorkingDirectory, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { assert, describe, it } from "@effect/vitest"; import * as ConfigProvider from "effect/ConfigProvider"; import * as Effect from "effect/Effect"; @@ -887,7 +883,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { return Effect.gen(function* () { const error = yield* runDevRunnerWithInput(devServerInput).pipe( Effect.provide(Layer.mergeAll(emptyConfigLayer, netServiceLayer, spawnerLayer)), - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), Effect.flip, ); @@ -953,7 +949,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { share: true, }).pipe( Effect.provide(Layer.mergeAll(emptyConfigLayer, netServiceLayer, spawnerLayer)), - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), ); assert.equal(spawnCount, 1); @@ -977,7 +973,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { host: "192.168.1.10", }).pipe( Effect.provide(Layer.mergeAll(emptyConfigLayer, netServiceLayer, spawnerLayer)), - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), Effect.flip, ); @@ -1012,7 +1008,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { host: "0.0.0.0", }).pipe( Effect.provide(Layer.mergeAll(emptyConfigLayer, netServiceLayer, spawnerLayer)), - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), ); assert.equal(spawnCount, 1); @@ -1037,7 +1033,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { host: "192.168.1.10", }).pipe( Effect.provide(Layer.mergeAll(emptyConfigLayer, netServiceLayer, spawnerLayer)), - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), ); assert.equal(spawnCount, 1); @@ -1097,9 +1093,9 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { share: true, }).pipe( Effect.provide(Layer.mergeAll(emptyConfigLayer, netServiceLayer, spawnerLayer)), - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), Effect.provideService( - HostProcessEnvironment, + HostProcess.Environment, input.ambientBundledDev === undefined ? {} : { T3CODE_BUNDLED_DEV: input.ambientBundledDev }, @@ -1144,8 +1140,8 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { port: undefined, }).pipe( Effect.provide(Layer.mergeAll(emptyConfigLayer, netServiceLayer, spawnerLayer)), - Effect.provideService(HostProcessPlatform, "linux"), - Effect.provideService(HostProcessEnvironment, {}), + Effect.provideService(HostProcess.Platform, "linux"), + Effect.provideService(HostProcess.Environment, {}), ); assert.equal(captured?.T3CODE_BUNDLED_DEV, undefined); @@ -1172,7 +1168,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { share: true, }).pipe( Effect.provide(Layer.mergeAll(emptyConfigLayer, netServiceLayer, spawnerLayer)), - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), ); assert.equal(spawnCount, 0); @@ -1188,7 +1184,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { return Effect.gen(function* () { const error = yield* runDevRunnerWithInput(devServerInput).pipe( Effect.provide(Layer.mergeAll(emptyConfigLayer, netServiceLayer, spawnerLayer)), - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), Effect.flip, ); @@ -1221,7 +1217,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { return Effect.gen(function* () { const error = yield* runDevRunnerWithInput(devServerInput).pipe( Effect.provide(Layer.mergeAll(emptyConfigLayer, netServiceLayer, spawnerLayer)), - Effect.provideService(HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), Effect.flip, ); @@ -1274,10 +1270,10 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { yield* runDevRunnerWithInput({ ...devServerInput, t3Home: input.t3Home }).pipe( Effect.provide(Layer.mergeAll(emptyConfigLayer, netServiceLayer, spawnerLayer)), - Effect.provideService(HostProcessPlatform, "linux"), - Effect.provideService(HostProcessWorkingDirectory, input.cwd), + Effect.provideService(HostProcess.Platform, "linux"), + Effect.provideService(HostProcess.WorkingDirectory, input.cwd), Effect.provideService( - HostProcessEnvironment, + HostProcess.Environment, input.ambientHome === undefined ? {} : { T3CODE_HOME: input.ambientHome }, ), ); diff --git a/scripts/dev-runner.ts b/scripts/dev-runner.ts index f026e2507e59..c8531d034c8f 100644 --- a/scripts/dev-runner.ts +++ b/scripts/dev-runner.ts @@ -6,7 +6,7 @@ import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; import * as NodeServices from "@effect/platform-node/NodeServices"; import * as NetService from "@t3tools/shared/Net"; import { resolveGitWorktreePath, resolveWorktreeT3Home } from "@t3tools/shared/devHome"; -import { HostProcessEnvironment, HostProcessWorkingDirectory } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; import * as Config from "effect/Config"; import * as Effect from "effect/Effect"; @@ -645,7 +645,7 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) { return yield* new DevRunnerHostNotProxiableError({ mode: input.mode, host: input.host }); } - const worktreePath = yield* resolveGitWorktreePath(yield* HostProcessWorkingDirectory); + const worktreePath = yield* resolveGitWorktreePath(yield* HostProcess.WorkingDirectory); const { offset, source } = yield* resolveOffset({ portOffset, @@ -663,11 +663,11 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) { checkPortAvailability: makeDefaultCheckPortAvailability(input.host), }); - const hostEnvironment = yield* HostProcessEnvironment; + const hostEnvironment = yield* HostProcess.Environment; // A dev server started inside a worktree defaults to that worktree's own // (gitignored) `.t3` — see @t3tools/shared/devHome for why this must // outrank an ambient T3CODE_HOME. `--home-dir` still wins. - const worktreeHome = yield* resolveWorktreeT3Home(yield* HostProcessWorkingDirectory); + const worktreeHome = yield* resolveWorktreeT3Home(yield* HostProcess.WorkingDirectory); // Trim before choosing: `--home-dir ""` is not a selection, and treating it // as one would skip the worktree default and land on the shared home — // exactly the outcome this precedence exists to prevent. diff --git a/scripts/export-brand-icons.ts b/scripts/export-brand-icons.ts index 6ae7cf3802ea..70cc0949e75c 100644 --- a/scripts/export-brand-icons.ts +++ b/scripts/export-brand-icons.ts @@ -2,7 +2,7 @@ import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; import * as NodeServices from "@effect/platform-node/NodeServices"; -import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Console from "effect/Console"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -408,7 +408,7 @@ const probeIconComposerTool = Effect.fn("iconExport.probeIconComposerTool")(func const resolveIconComposerTool = Effect.fn("iconExport.resolveIconComposerTool")(function* () { const path = yield* Path.Path; - const environment = yield* HostProcessEnvironment; + const environment = yield* HostProcess.Environment; const configuredTool = environment.ICON_COMPOSER_TOOL?.trim(); if (configuredTool) { const tool = yield* probeIconComposerTool(configuredTool); diff --git a/scripts/install.test.ts b/scripts/install.test.ts index 9ded5cb51166..e0fb60517e4a 100644 --- a/scripts/install.test.ts +++ b/scripts/install.test.ts @@ -1,5 +1,5 @@ // @effect-diagnostics nodeBuiltinImport:off - Drives the real shell installer through a PTY and a gated HTTP fixture. -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as NodeChildProcess from "node:child_process"; import * as NodeCrypto from "node:crypto"; import * as NodeFSP from "node:fs/promises"; @@ -9,13 +9,13 @@ import * as NodePath from "node:path"; import { describe, expect, it } from "vite-plus/test"; // util-linux's script gives the real installer a terminal without a browser or extra packages. -describe.skipIf(HostProcessPlatform.defaultValue() !== "linux")("installer terminal", () => { +describe.skipIf(HostProcess.Platform.defaultValue() !== "linux")("installer terminal", () => { it.each([false, true])( "preserves download and install behavior (HTTP failure: %s)", async (fail) => { const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-install-progress-")); const version = "1.2.3"; - const stem = `t3-${version}-linux-${HostProcessArchitecture.defaultValue()}`; + const stem = `t3-${version}-linux-${HostProcess.Architecture.defaultValue()}`; const archiveName = `${stem}.tar.gz`; let resumeDownload: (() => void) | undefined; let sawPartialProgress = false; diff --git a/scripts/lib/build-target-arch.test.ts b/scripts/lib/build-target-arch.test.ts index 5da97047570b..d86573dca5a9 100644 --- a/scripts/lib/build-target-arch.test.ts +++ b/scripts/lib/build-target-arch.test.ts @@ -2,7 +2,7 @@ import { assert, describe, it } from "@effect/vitest"; import * as ConfigProvider from "effect/ConfigProvider"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { getDefaultBuildArch } from "./build-target-arch.ts"; @@ -18,8 +18,8 @@ const withHostRuntime = ( ) => Effect.provide( Layer.mergeAll( - Layer.succeed(HostProcessPlatform, platform), - Layer.succeed(HostProcessArchitecture, arch), + Layer.succeed(HostProcess.Platform, platform), + Layer.succeed(HostProcess.Architecture, arch), ConfigProvider.layer(ConfigProvider.fromEnv({ env: compactEnv(env) })), ), ); diff --git a/scripts/lib/build-target-arch.ts b/scripts/lib/build-target-arch.ts index b79c1746f88f..67e379c202ab 100644 --- a/scripts/lib/build-target-arch.ts +++ b/scripts/lib/build-target-arch.ts @@ -1,4 +1,4 @@ -import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Config from "effect/Config"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; @@ -27,8 +27,8 @@ const optionToUndefined = (value: Option.Option): A | undefined => Option.getOrUndefined(value); const resolveHostProcessArch = Effect.fn("resolveHostProcessArch")(function* () { - const platform = yield* HostProcessPlatform; - const processArch = yield* HostProcessArchitecture; + const platform = yield* HostProcess.Platform; + const processArch = yield* HostProcess.Architecture; if (processArch === "arm64") return "arm64"; if (processArch === "x64") { if (platform !== "win32") return "x64"; diff --git a/scripts/lib/cli-external-packages.test.ts b/scripts/lib/cli-external-packages.test.ts index 33c2e897b047..b66545c941fb 100644 --- a/scripts/lib/cli-external-packages.test.ts +++ b/scripts/lib/cli-external-packages.test.ts @@ -34,7 +34,7 @@ const decodeManifest = Schema.decodeUnknownSync(Schema.fromJsonString(PackageMan describe("shouldBundleCliDependency", () => { it("bundles ordinary runtime dependencies", () => { - for (const id of ["effect", "@effect/platform", "hono", "@t3tools/shared/hostProcess"]) { + for (const id of ["effect", "@effect/platform", "hono", "@t3tools/shared/HostProcess"]) { assert.strictEqual(shouldBundleCliDependency(id), true, id); } }); diff --git a/scripts/mobile-native-client.ts b/scripts/mobile-native-client.ts index 6d9f1c762a63..9d33fe2eb293 100644 --- a/scripts/mobile-native-client.ts +++ b/scripts/mobile-native-client.ts @@ -1,10 +1,6 @@ import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; import * as NodeServices from "@effect/platform-node/NodeServices"; -import { - HostProcessEnvironment, - HostProcessExecutablePath, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { isCommandAvailable, resolveSpawnCommand } from "@t3tools/shared/shell"; import * as Console from "effect/Console"; import * as Crypto from "effect/Crypto"; @@ -155,9 +151,9 @@ const collect = (stream: Stream.Stream) => ); export const resolveAdb = Effect.gen(function* () { if (yield* isCommandAvailable("adb")) return "adb"; - const environment = yield* HostProcessEnvironment; + const environment = yield* HostProcess.Environment; const path = yield* Path.Path; - const executable = (yield* HostProcessPlatform) === "win32" ? "adb.exe" : "adb"; + const executable = (yield* HostProcess.Platform) === "win32" ? "adb.exe" : "adb"; for (const sdk of [environment.ANDROID_SDK_ROOT, environment.ANDROID_HOME]) { if (!sdk) continue; const candidate = path.join(sdk, "platform-tools", executable); @@ -176,7 +172,7 @@ const command = Effect.fn("nativeClient.command")(function* ( cwd?: string, ) { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const environment = yield* HostProcessEnvironment; + const environment = yield* HostProcess.Environment; const spawn = yield* resolveSpawnCommand(program === "adb" ? yield* resolveAdb : program, args); const child = yield* spawner.spawn( ChildProcess.make(spawn.command, spawn.args, { @@ -207,7 +203,7 @@ const command = Effect.fn("nativeClient.command")(function* ( }, Effect.scoped); const fingerprint = Effect.fn("nativeClient.fingerprint")(function* (platform: NativePlatform) { - const output = yield* command(yield* HostProcessExecutablePath, [ + const output = yield* command(yield* HostProcess.ExecutablePath, [ "--eval", `require('expo/fingerprint').createFingerprintAsync(process.cwd(), { platforms: [process.argv[1]], silent: true }).then(fp => console.log('T3_NATIVE_FINGERPRINT=' + fp.hash)).catch(e => { console.error(e); process.exitCode = 1; });`, platform, @@ -226,7 +222,7 @@ const validateDevice = Effect.fn("nativeClient.validateDevice")(function* ( device: string, ) { if (platform === "ios") { - if ((yield* HostProcessPlatform) !== "darwin") + if ((yield* HostProcess.Platform) !== "darwin") return yield* new NativeClientError({ message: "Run iOS check/ensure on the Mac that hosts the simulator.", }); @@ -299,7 +295,7 @@ const main = Command.make( yield* validateDevice(platform, device); const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const environment = yield* HostProcessEnvironment; + const environment = yield* HostProcess.Environment; const home = environment.HOME ?? environment.USERPROFILE; if (!home) return yield* new NativeClientError({ diff --git a/scripts/mobile-native-static-check.test.ts b/scripts/mobile-native-static-check.test.ts index a3e4b183b3ac..32a90c85d51b 100644 --- a/scripts/mobile-native-static-check.test.ts +++ b/scripts/mobile-native-static-check.test.ts @@ -1,5 +1,5 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; -import * as HostProcess from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { assert, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -33,7 +33,7 @@ const provideSpawner = (spawn: ChildProcessSpawner.ChildProcessSpawner["Service" Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(spawn)); const runSwiftLint = runCommand("swiftlint", ["lint", "--strict"], "/repo/apps/mobile").pipe( - Effect.provideService(HostProcess.HostProcessPlatform, "linux"), + Effect.provideService(HostProcess.Platform, "linux"), ); it.layer(NodeServices.layer)("mobile native source discovery", (it) => { diff --git a/scripts/smoke-cli-archive.ts b/scripts/smoke-cli-archive.ts index 179c268b909a..9780680cd9e1 100644 --- a/scripts/smoke-cli-archive.ts +++ b/scripts/smoke-cli-archive.ts @@ -22,7 +22,7 @@ import { Command, Flag } from "effect/cli"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; import * as NetService from "@t3tools/shared/Net"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { windowsSystemTar } from "./build-cli-archive.ts"; import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http"; @@ -72,7 +72,7 @@ const smokeCliArchive = Effect.fn("smokeCliArchive")(function* (input: { }) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const platform = yield* HostProcessPlatform; + const platform = yield* HostProcess.Platform; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const scratch = yield* fs.makeTempDirectory({ prefix: "t3-cli-smoke-" }); // Windows can keep t3.exe locked (EBUSY) for a moment after the server diff --git a/vite.config.ts b/vite.config.ts index 46d61de5e963..b5d906697a6d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -219,7 +219,7 @@ export default defineConfig({ }, { // The one place that reads the host platform to seed the injected references. - files: ["packages/shared/src/hostProcess.ts"], + files: ["packages/shared/src/HostProcess.ts"], rules: { "t3code/no-global-process-runtime": "off" }, }, { From 6cd02e98f7edd78b7bde9ceff26c829476dbeb77 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 9 Oct 2026 16:40:10 -0700 Subject: [PATCH 29/39] feat(web): filter PR comments by bots and resolved threads (#17645) Co-authored-by: Claude Opus 5.5 --- .../PullRequestSummaryTab.test.tsx | 147 ++++---- .../pullRequest/PullRequestSummaryTab.tsx | 327 +++++++----------- 2 files changed, 205 insertions(+), 269 deletions(-) diff --git a/apps/web/src/components/pullRequest/PullRequestSummaryTab.test.tsx b/apps/web/src/components/pullRequest/PullRequestSummaryTab.test.tsx index 57033b67154c..27191a970b3e 100644 --- a/apps/web/src/components/pullRequest/PullRequestSummaryTab.test.tsx +++ b/apps/web/src/components/pullRequest/PullRequestSummaryTab.test.tsx @@ -9,6 +9,14 @@ vi.mock("~/browser/useOpenLink", () => ({ useOpenLink: () => vi.fn() })); vi.mock("./PullRequestMarkdown", () => ({ PullRequestMarkdown: ({ text }: { text: string }) =>

{text}

, })); +vi.mock("../ui/menu", () => ({ + Menu: "div", + MenuCheckboxItem: "div", + MenuGroup: "div", + MenuGroupLabel: "div", + MenuPopup: "div", + MenuTrigger: "div", +})); vi.mock("../ui/tooltip", () => ({ Tooltip: ({ children }: { children: ReactNode }) => children, TooltipTrigger: ({ children }: { children: ReactNode }) => children, @@ -70,7 +78,20 @@ const detail: PullRequestDetailView = { }; let renderer: ReactTestRenderer; -beforeEach(() => vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true)); +beforeEach(() => { + vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); + const store = new Map(); + vi.stubGlobal( + "window", + Object.assign(new EventTarget(), { + localStorage: { + getItem: (key: string) => store.get(key) ?? null, + setItem: (key: string, value: string) => store.set(key, value), + removeItem: (key: string) => store.delete(key), + }, + }), + ); +}); afterEach(() => { act(() => renderer?.unmount()); vi.unstubAllGlobals(); @@ -145,70 +166,80 @@ it("keeps an unsaved description when collapsed and reopened", () => { expect(renderer.root.findByType("textarea").props.value).toBe("Unsaved description"); }); -it("opens bot reports in pages without hiding human comments", () => { +function texts() { + return renderer.root + .findAllByType("p") + .map((p) => p.children.join("")) + .filter((text) => text !== detail.body); +} + +function filterItem(label: string) { + return renderer.root.find( + (node) => typeof node.props.onCheckedChange === "function" && node.children.includes(label), + ); +} + +it("hides bot and resolved comments until the filter shows them, and remembers the choice", () => { + const comment = (index: number, login: string, body: string, isBot = false) => ({ + id: `comment-${index}`, + kind: "issue-comment" as const, + author: { login, name: null, avatarUrl: null, isBot }, + body, + createdAt: `2026-09-01T00:00:${String(index).padStart(2, "0")}Z`, + url: null, + path: null, + reviewState: null, + }); + const resolved = { + ...comment(13, "reviewer", "Resolved remark"), + kind: "review-comment" as const, + }; const value: PullRequestDetailView = { ...detail, - commentCount: 13, - comments: Array.from({ length: 13 }, (_, index) => ({ - id: `comment-${index}`, - kind: "issue-comment", - author: { - login: index === 12 ? "human" : "review-app", - name: null, - avatarUrl: null, - isBot: index !== 12, + commentCount: 14, + comments: [ + ...Array.from({ length: 12 }, (_, index) => + comment(index, "review-app", `Bot report ${index}`, true), + ), + comment(12, "human", "Human comment"), + resolved, + ], + reviewThreads: [ + { + id: "thread", + path: "src/index.ts", + line: 1, + side: "right", + isResolved: true, + isOutdated: false, + comments: [resolved], }, - body: index === 12 ? "Human comment" : `Bot report ${index}`, - createdAt: `2026-09-01T00:00:${String(index).padStart(2, "0")}Z`, - url: null, - path: null, - reviewState: null, - })), + ], }; act(() => { renderer = create(render(value)); }); - expect(renderer.root.findAllByType("p").map((p) => p.children.join(""))).toContain( - "Human comment", - ); - expect( - renderer.root.findAllByType("p").some((p) => p.children.join("").startsWith("Bot report")), - ).toBe(false); - const group = renderer.root - .findAllByType("button") - .find((button) => button.props["aria-label"] === "12 bot comments")!; - act(() => group.props.onClick({ nativeEvent: {}, preventDefault() {}, stopPropagation() {} })); + expect(texts()).toEqual(["Human comment"]); expect( - renderer.root.findAllByType("p").filter((p) => p.children.join("").startsWith("Bot report")), - ).toHaveLength(10); - expect(renderer.root.findAllByType("p").map((p) => p.children.join(""))).not.toContain( - "Bot report 0", - ); - act(() => - renderer.root - .findAllByType("button") - .find((button) => button.children.includes(" older bot comment"))! - .props.onClick(), - ); - expect( - renderer.root.findAllByType("p").filter((p) => p.children.join("").startsWith("Bot report")), - ).toHaveLength(12); - act(() => group.props.onClick({ nativeEvent: {}, preventDefault() {}, stopPropagation() {} })); - act(() => group.props.onClick({ nativeEvent: {}, preventDefault() {}, stopPropagation() {} })); - expect(renderer.root.findAllByType("p").map((p) => p.children.join(""))).toContain( - "Bot report 0", - ); - act(() => - renderer.root - .findAllByType("button") - .find((button) => button.children.includes(" recent bot comments"))! - .props.onClick(), - ); - expect( - renderer.root.findAllByType("p").filter((p) => p.children.join("").startsWith("Bot report")), - ).toHaveLength(10); - act(() => renderer.update(render({ ...value, url: `${value.url}0`, number: 10 }))); + renderer.root.findByProps({ "aria-label": "Filter comments" }).children.join(""), + ).toContain("13 hidden"); + + act(() => filterItem("Bot comments").props.onCheckedChange(true)); + // Bots now share the human comment's pages: the ten most recent, the oldest left behind. + expect(texts().filter((text) => text.startsWith("Bot report"))).toHaveLength(9); + expect(texts()).toContain("Human comment"); + expect(texts()).not.toContain("Bot report 0"); + + act(() => filterItem("Resolved or dismissed").props.onCheckedChange(true)); expect( - renderer.root.findAllByType("p").some((p) => p.children.join("").startsWith("Bot report")), - ).toBe(false); + renderer.root.findAllByType("button").some((button) => button.children.includes("Resolved")), + ).toBe(true); + + // Another pull request, or the same one opened again, keeps the reader's choice. + act(() => renderer.unmount()); + act(() => { + renderer = create(render({ ...value, url: `${value.url}0`, number: 10 })); + }); + expect(filterItem("Bot comments").props.checked).toBe(true); + expect(texts()).toContain("Bot report 11"); }); diff --git a/apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx b/apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx index 581c4d0741d7..9fae094ce564 100644 --- a/apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx +++ b/apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx @@ -12,12 +12,15 @@ import { ChevronDownIcon, ChevronRightIcon, HammerIcon, + ListFilterIcon, TagIcon, UsersIcon, } from "lucide-react"; +import * as Schema from "effect/Schema"; import { useRef, useState, type ReactNode } from "react"; import { pullRequestEnvironment } from "~/state/pullRequests"; +import { useLocalStorage } from "~/hooks/useLocalStorage"; import { cn } from "~/lib/utils"; import { useOpenLink } from "~/browser/useOpenLink"; import { formatRelativeTimeLabel } from "~/timestampFormat"; @@ -25,6 +28,14 @@ import { formatRelativeTimeLabel } from "~/timestampFormat"; import { Button } from "../ui/button"; import { PullRequestEditButton } from "./PullRequestEditButton"; import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible"; +import { + Menu, + MenuCheckboxItem, + MenuGroup, + MenuGroupLabel, + MenuPopup, + MenuTrigger, +} from "../ui/menu"; import { toastManager } from "../ui/toast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { @@ -349,111 +360,21 @@ function Section({ ); } -function CommentGroup({ - label, - comments, - detail, - children, - onOpenChange, -}: { - label: string; - comments: readonly PullRequestComment[]; - detail: PullRequestDetailView; - children: ReactNode; - onOpenChange?: (open: boolean) => void; -}) { - const authors = [ - ...new Map( - comments.map((comment) => [reviewerKey(comment.author?.login ?? "ghost"), comment.author]), - ).values(), - ]; - const fileCount = new Set(comments.flatMap((comment) => (comment.path ? [comment.path] : []))) - .size; - const latest = comments.reduce( - (date, comment) => (date === null || comment.createdAt > date ? comment.createdAt : date), - null, - ); - return ( -
- -
-
- {authors.slice(0, 3).map((actor) => ( - // The ring separates the overlapping faces; it belongs to the stack, not the actor. - - - - ))} - {authors.length > 3 ? ( - - +{authors.length - 3} - - ) : null} -
- - - {label} - - - {authors.length} {authors.length === 1 ? "author" : "authors"} - - {fileCount > 0 ? ( - - · {fileCount} {fileCount === 1 ? "file" : "files"} - - ) : null} - {latest ? ( - - · Latest{" "} - - }> - {formatRelativeTimeLabel(latest)} - - {new Date(latest).toLocaleString()} - - - ) : null} - - - - -
- -
{children}
-
-
-
- ); -} - /** * What a first render of the conversation carries. A pull request with two hundred comments is * two hundred markdown documents, and the ones worth arriving for are the recent ones. */ const COMMENT_PAGE = 10; +/** + * Which kinds of comment the conversation shows. Kept per browser rather than per pull request: + * whether a reader wants bot reports and finished threads in front of them is a habit, not a + * property of one change. + */ +const COMMENT_FILTER_STORAGE_KEY = "t3code:pull-request-comment-filter:v1"; +const CommentFilterSchema = Schema.Struct({ bots: Schema.Boolean, resolved: Schema.Boolean }); +const DEFAULT_COMMENT_FILTER: typeof CommentFilterSchema.Type = { bots: false, resolved: false }; + export function PullRequestSummaryTab({ environmentId, threadRef, @@ -487,10 +408,12 @@ export function PullRequestSummaryTab({ // Keyed by the pull request, so opening another one starts at the end of its conversation // rather than wherever the last one had been read back to. const [shown, setShown] = useState({ url: detail.url, count: COMMENT_PAGE }); - const [openedBotGroup, setOpenedBotGroup] = useState(null); - const [shownBots, setShownBots] = useState({ url: detail.url, count: COMMENT_PAGE }); - const shownBotComments = shownBots.url === detail.url ? shownBots.count : COMMENT_PAGE; const shownComments = shown.url === detail.url ? shown.count : COMMENT_PAGE; + const [commentFilter, setCommentFilter] = useLocalStorage( + COMMENT_FILTER_STORAGE_KEY, + DEFAULT_COMMENT_FILTER, + CommentFilterSchema, + ); // A comment that already lives on a review thread is that thread: the thread carries the line // and side the bare comment has lost, and a resolved one is finished work nobody should be // invited to fix again — the same call the whole-review hand-off makes. @@ -500,22 +423,24 @@ export function PullRequestSummaryTab({ ), ); - const activeComments: PullRequestComment[] = []; - const finishedComments: PullRequestComment[] = []; - const botComments: PullRequestComment[] = []; - for (const comment of detail.comments) { - const finished = - threadByCommentId.get(comment.id)?.isResolved || - pullRequestReviewOutcome(comment.reviewState) === "dismissed"; - const bot = comment.author?.isBot === true || comment.author?.login.endsWith("[bot]"); - (finished ? finishedComments : bot ? botComments : activeComments).push(comment); - } + const isFinished = (comment: PullRequestComment) => + threadByCommentId.get(comment.id)?.isResolved === true || + pullRequestReviewOutcome(comment.reviewState) === "dismissed"; + const isBot = (comment: PullRequestComment) => + comment.author?.isBot === true || comment.author?.login.endsWith("[bot]") === true; + const filteredComments = detail.comments.filter( + (comment) => + (commentFilter.bots || !isBot(comment)) && (commentFilter.resolved || !isFinished(comment)), + ); + // The one count the filter shows. Per-kind counts would overlap, since a resolved bot comment + // is both kinds and needs both shown. + const filteredOutCount = detail.comments.length - filteredComments.length; // Windowed by recency regardless of display order: expanding always reaches further back in // time, whether the newest comment currently reads first or last. - const recentComments = activeComments.slice(Math.max(0, activeComments.length - shownComments)); - const hiddenCommentCount = activeComments.length - recentComments.length; - const recentBotComments = botComments.slice(Math.max(0, botComments.length - shownBotComments)); - const hiddenBotCommentCount = botComments.length - recentBotComments.length; + const recentComments = filteredComments.slice( + Math.max(0, filteredComments.length - shownComments), + ); + const hiddenCommentCount = filteredComments.length - recentComments.length; const [commentOrder, setCommentOrder] = useState<"newest" | "oldest">("newest"); const visibleComments = orderPullRequestComments(recentComments, commentOrder); const showOldestCommentsButton = @@ -638,6 +563,30 @@ export function PullRequestSummaryTab({ const renderComment = (comment: PullRequestComment) => { const thread = threadByCommentId.get(comment.id); const body = visibleBody(comment.body); + if (isFinished(comment)) { + return ( + + } + /> + ); + } const outcome = pullRequestReviewOutcome(comment.reviewState); // An approval is a verdict, not a finding: there is nothing in it to fix. const finding: PullRequestFinding | null = @@ -920,20 +869,51 @@ export function PullRequestSummaryTab({
setCommentOrder((value) => (value === "newest" ? "oldest" : "newest"))} - > - - {commentOrder === "newest" ? "Newest first" : "Oldest first"} - +
+ + } + aria-label="Filter comments" + > + + {filteredOutCount > 0 ? `${filteredOutCount} hidden` : "Filter"} + + + + Show + setCommentFilter((value) => ({ ...value, bots }))} + > + Bot comments + + + setCommentFilter((value) => ({ ...value, resolved })) + } + > + Resolved or dismissed + + + + + +
} > {activityPending ? ( @@ -950,6 +930,12 @@ export function PullRequestSummaryTab({ ) : null} {detail.comments.length === 0 ? (

No comments yet.

+ ) : filteredComments.length === 0 ? ( +

+ {filteredOutCount === 1 + ? "The only comment is hidden by the filter." + : `All ${filteredOutCount} comments are hidden by the filter.`} +

) : (
{commentOrder === "oldest" ? showOldestCommentsButton : null} @@ -965,87 +951,6 @@ export function PullRequestSummaryTab({ Show only {COMMENT_PAGE} recent comments ) : null} - {botComments.length > 0 ? ( - { - if (open) setOpenedBotGroup(detail.url); - }} - > -
- {openedBotGroup === detail.url - ? orderPullRequestComments(recentBotComments, commentOrder).map( - renderComment, - ) - : null} - {hiddenBotCommentCount > 0 ? ( - - ) : null} - {shownBotComments > COMMENT_PAGE ? ( - - ) : null} -
-
- ) : null} - {finishedComments.length > 0 ? ( - -
- {orderPullRequestComments(finishedComments, commentOrder).map((comment) => { - const thread = threadByCommentId.get(comment.id); - return ( - - } - /> - ); - })} -
-
- ) : null}
)} From 0c4012055deacd43876b80902caad6dd7d6a2eef Mon Sep 17 00:00:00 2001 From: Exotic <118054752+extoci@users.noreply.github.com> Date: Sat, 10 Oct 2026 02:45:38 +0300 Subject: [PATCH 30/39] fix(clients): remove redundant prefix from PR watch status (#17635) --- packages/client-runtime/src/state/threadExecution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client-runtime/src/state/threadExecution.ts b/packages/client-runtime/src/state/threadExecution.ts index bef28ee72510..b1e2c2731228 100644 --- a/packages/client-runtime/src/state/threadExecution.ts +++ b/packages/client-runtime/src/state/threadExecution.ts @@ -362,7 +362,7 @@ export function presentPendingBackgroundWork( const named = only.label !== noun; const title = waiting ? named - ? `Waiting on ${noun} ${only.label}` + ? `${only.kind === "monitor" ? "" : `Waiting on ${noun} `}${only.label}` : `Waiting on a ${noun}` : named ? `Running: ${only.label}` From 99d2651dd5f82eeed800672e371427d7266dfe72 Mon Sep 17 00:00:00 2001 From: maria Date: Fri, 9 Oct 2026 20:46:38 -0300 Subject: [PATCH 31/39] fix(web): pending requests wait until you stop typing (#17637) --- apps/web/src/components/ChatView.tsx | 4 +- apps/web/src/components/chat/ChatComposer.tsx | 55 ++++++++++-- .../chat/composerTypingGuard.test.ts | 89 +++++++++++++++++++ .../components/chat/composerTypingGuard.ts | 51 +++++++++++ .../components/chat/useComposerTypingGuard.ts | 51 +++++++++++ 5 files changed, 242 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/components/chat/composerTypingGuard.test.ts create mode 100644 apps/web/src/components/chat/composerTypingGuard.ts create mode 100644 apps/web/src/components/chat/useComposerTypingGuard.ts diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 1fe19f6bc2f2..c8c202f5cadc 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -8808,7 +8808,8 @@ export default function ChatView(props: ChatViewProps) { }); return; } - if (activePendingProgress) { + const sendCtx = composerRef.current?.getSendContext(); + if (activePendingProgress && sendCtx?.answeringPendingUserInput !== false) { if (directAnnotation) { notifyDirectAnnotationAttached(); return; @@ -8816,7 +8817,6 @@ export default function ChatView(props: ChatViewProps) { onAdvanceActivePendingUserInput(); return; } - const sendCtx = composerRef.current?.getSendContext(); if (!sendCtx?.providerAvailable) { notifyDirectAnnotationAttached(); return; diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 82a34a840c2f..221b7a57738b 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -1,4 +1,5 @@ import { formatProviderSkillDisplayName } from "@t3tools/shared/inlineSkills"; +import { useComposerTypingGuard } from "./useComposerTypingGuard"; import { DESKTOP_PASTE_AS_TEXT_EVENT } from "../../lib/desktopPasteAsText"; import { runtimeModeConfig, runtimeModeOptions as runtimeModes } from "./runtimeModeConfig"; import { isLocalEnvironmentDisabled } from "../../localEnvironment"; @@ -1485,6 +1486,7 @@ export interface ChatComposerHandle { addTerminalContext: (selection: TerminalContextSelection) => void; /** Get the current prompt/effort/model state for use in send. */ getSendContext: () => { + answeringPendingUserInput: boolean; prompt: string; images: ComposerImageAttachment[]; files: ComposerFileAttachment[]; @@ -1737,12 +1739,12 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) sendDisabledReason: externalSendDisabledReason, isPreparingWorktree, environmentUnavailable, - activePendingApproval, + activePendingApproval: incomingPendingApproval, pendingApprovals, - pendingUserInputs, - activePendingProgress, + pendingUserInputs: incomingPendingUserInputs, + activePendingProgress: incomingPendingProgress, activePendingResolvedAnswers, - activePendingIsResponding, + activePendingIsResponding: incomingPendingIsResponding, activePendingDraftAnswers, activePendingQuestionIndex, respondingRequestIds, @@ -1808,9 +1810,30 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) editingQueuedAttachments, onRemoveEditingQueuedAttachment, } = props; - const isLiteralPendingAnswer = activePendingProgress?.activeQuestion?.initialAnswer !== undefined; const primaryEnvironmentId = usePrimaryEnvironmentId(); const composerDraftTargetKey = composerTargetKey(composerDraftTarget); + const { + heldRequestIds, + onDraftChange: onTypingGuardDraftChange, + onFocus: onTypingGuardFocus, + onBlur: onTypingGuardBlur, + onSend: onTypingGuardSend, + } = useComposerTypingGuard(composerDraftTargetKey, [ + ...pendingApprovals.map((request) => request.requestId), + ...incomingPendingUserInputs.map((request) => request.requestId), + ]); + // Hold composer takeover while the user finishes their thread draft. + const activePendingApproval = + incomingPendingApproval && !heldRequestIds.has(incomingPendingApproval.requestId) + ? incomingPendingApproval + : null; + const holdingUserInput = incomingPendingUserInputs[0] + ? heldRequestIds.has(incomingPendingUserInputs[0].requestId) + : false; + const pendingUserInputs = holdingUserInput ? [] : incomingPendingUserInputs; + const activePendingProgress = holdingUserInput ? null : incomingPendingProgress; + const activePendingIsResponding = !holdingUserInput && incomingPendingIsResponding; + const isLiteralPendingAnswer = activePendingProgress?.activeQuestion?.initialAnswer !== undefined; // Opening a running thread resyncs for a few frames. Show the sync row, and // hide the tasks row for it, only when the sync lasts. Logic that depends on // the real phase keeps reading `props.threadSyncPhase`. @@ -3712,6 +3735,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ); return; } + onTypingGuardDraftChange(); promptRef.current = nextPrompt; setPrompt(nextPrompt); // Any edit ends browsing, even one later undone by hand: typing a @@ -3818,6 +3842,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) [ activePendingProgress?.activeQuestion, expandComposerForEditorChange, + onTypingGuardDraftChange, pendingUserInputs.length, onChangeActivePendingUserInputCustomAnswer, promptRef, @@ -3909,6 +3934,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ); } else { setPrompt(next.text); + onTypingGuardDraftChange(); } setComposerCursor(nextCursor); setComposerTrigger( @@ -3931,6 +3957,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) activePendingUserInput, isLiteralPendingAnswer, onChangeActivePendingUserInputCustomAnswer, + onTypingGuardDraftChange, promptRef, setPrompt, setComposerTrigger, @@ -4269,6 +4296,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) }); setComposerSubmissionError(submission.validationMessage); if (!submission.didDispatch) return; + onTypingGuardSend(); if (shouldBlurMobileComposerOnSubmit()) { blurMobileComposerAfterSend(); } @@ -4277,6 +4305,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) activeThreadId, activePendingProgress, attachmentTargetKey, + onTypingGuardSend, blurMobileComposerAfterSend, environmentId, isSendDisabled, @@ -6532,6 +6561,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) }); }, getSendContext: () => ({ + answeringPendingUserInput: activePendingProgress !== null, prompt: promptRef.current, images: composerImagesRef.current, files: composerFilesRef.current, @@ -6578,6 +6608,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) foldPastedText, composerDraftTarget, composerCursor, + activePendingProgress, composerTerminalContexts, insertComposerDraftTerminalContext, insertComposerText, @@ -6652,6 +6683,12 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) }} onFocusCapture={(event) => { const activeElement = event.target; + if ( + activeElement instanceof Element && + activeElement.closest('[data-testid="composer-editor"]') + ) { + onTypingGuardFocus(); + } if (composerControlsCollapsed && isInsideRestingComposerControlScope(activeElement)) { return; } @@ -6670,7 +6707,13 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) } setIsComposerFocused(true); }} - onBlurCapture={() => { + onBlurCapture={(event) => { + if ( + event.target instanceof Element && + event.target.closest('[data-testid="composer-editor"]') + ) { + onTypingGuardBlur(); + } scheduleComposerCollapseCheck(); }} onDragEnterCapture={(event) => { diff --git a/apps/web/src/components/chat/composerTypingGuard.test.ts b/apps/web/src/components/chat/composerTypingGuard.test.ts new file mode 100644 index 000000000000..9c43665e3078 --- /dev/null +++ b/apps/web/src/components/chat/composerTypingGuard.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it } from "vite-plus/test"; +import { createComposerTypingGuardState, updateComposerTypingGuard } from "./composerTypingGuard"; + +function typing() { + return updateComposerTypingGuard( + updateComposerTypingGuard(createComposerTypingGuardState(), { type: "focus" }), + { type: "type", now: 1000 }, + ); +} + +describe("composer typing guard", () => { + it("holds new requests while the focused draft was recently edited", () => { + const state = updateComposerTypingGuard(typing(), { + type: "requests", + requestIds: ["approval", "question"], + now: 2499, + }); + expect([...state.held]).toEqual(["approval", "question"]); + }); + + it.each(["idle", "send", "blur"] as const)( + "releases on %s and never re-holds the request", + (type) => { + let state = updateComposerTypingGuard(typing(), { + type: "requests", + requestIds: ["question"], + now: 1200, + }); + state = updateComposerTypingGuard(state, { type }); + expect([...state.held]).toEqual([]); + state = updateComposerTypingGuard(state, { type: "focus" }); + state = updateComposerTypingGuard(state, { type: "type", now: 2000 }); + state = updateComposerTypingGuard(state, { type: "requests", requestIds: [], now: 2100 }); + state = updateComposerTypingGuard(state, { + type: "requests", + requestIds: ["question", "new-question"], + now: 2200, + }); + expect([...state.held]).toEqual(["new-question"]); + }, + ); + + it("shows requests immediately when idle or unfocused", () => { + for (const state of [ + createComposerTypingGuardState(), + typing(), + updateComposerTypingGuard(typing(), { type: "blur" }), + ]) { + expect([ + ...updateComposerTypingGuard(state, { + type: "requests", + requestIds: ["question"], + now: 2500, + }).held, + ]).toEqual([]); + } + }); + + it("does not hide a request that already took over when typing starts", () => { + let state = updateComposerTypingGuard(createComposerTypingGuardState(), { + type: "requests", + requestIds: ["question"], + now: 0, + }); + state = updateComposerTypingGuard(state, { type: "focus" }); + state = updateComposerTypingGuard(state, { type: "type", now: 1000 }); + state = updateComposerTypingGuard(state, { + type: "requests", + requestIds: ["question"], + now: 1100, + }); + expect([...state.held]).toEqual([]); + }); + + it("drops a hold when its request is resolved", () => { + const held = updateComposerTypingGuard(typing(), { + type: "requests", + requestIds: ["question"], + now: 1100, + }); + expect([ + ...updateComposerTypingGuard(held, { + type: "requests", + requestIds: [], + now: 1200, + }).held, + ]).toEqual([]); + }); +}); diff --git a/apps/web/src/components/chat/composerTypingGuard.ts b/apps/web/src/components/chat/composerTypingGuard.ts new file mode 100644 index 000000000000..01c9cca77d4c --- /dev/null +++ b/apps/web/src/components/chat/composerTypingGuard.ts @@ -0,0 +1,51 @@ +export const COMPOSER_TYPING_IDLE_MS = 1500; + +export function createComposerTypingGuardState() { + return { + seen: new Set(), + held: new Set(), + focused: false, + lastTypedAt: -Infinity, + }; +} + +type TypingGuardEvent = + | { type: "requests"; requestIds: readonly string[]; now: number } + | { type: "type"; now: number } + | { type: "focus" | "blur" | "send" | "idle" }; + +export function updateComposerTypingGuard( + state: ReturnType, + event: TypingGuardEvent, +) { + switch (event.type) { + case "requests": { + const seen = new Set(state.seen); + const held = new Set([...state.held].filter((id) => event.requestIds.includes(id))); + for (const id of event.requestIds) { + if ( + !seen.has(id) && + state.focused && + event.now - state.lastTypedAt < COMPOSER_TYPING_IDLE_MS + ) { + held.add(id); + } + seen.add(id); + } + return { ...state, seen, held }; + } + case "type": + return { ...state, lastTypedAt: event.now }; + case "focus": + return { ...state, focused: true }; + case "blur": + case "send": + case "idle": + return { + ...state, + held: new Set(), + lastTypedAt: -Infinity, + focused: event.type === "blur" ? false : state.focused, + }; + } +} diff --git a/apps/web/src/components/chat/useComposerTypingGuard.ts b/apps/web/src/components/chat/useComposerTypingGuard.ts new file mode 100644 index 000000000000..cf3de772b378 --- /dev/null +++ b/apps/web/src/components/chat/useComposerTypingGuard.ts @@ -0,0 +1,51 @@ +import { useCallback, useEffect, useState } from "react"; +import { + COMPOSER_TYPING_IDLE_MS, + createComposerTypingGuardState, + updateComposerTypingGuard, +} from "./composerTypingGuard"; + +export function useComposerTypingGuard(scope: string, requestIds: readonly string[]) { + const requestsKey = JSON.stringify(requestIds); + const [snapshot, setSnapshot] = useState(() => ({ + scope, + requestsKey, + state: updateComposerTypingGuard(createComposerTypingGuardState(), { + type: "requests", + requestIds, + now: Date.now(), + }), + })); + const state = snapshot.state; + if (snapshot.scope !== scope || snapshot.requestsKey !== requestsKey) { + setSnapshot((current) => ({ + scope, + requestsKey, + state: updateComposerTypingGuard( + current.scope === scope + ? current.state + : { ...createComposerTypingGuardState(), focused: current.state.focused }, + { type: "requests", requestIds, now: Date.now() }, + ), + })); + } + const dispatch = useCallback((event: Parameters[1]) => { + setSnapshot((current) => ({ + ...current, + state: updateComposerTypingGuard(current.state, event), + })); + }, []); + const onDraftChange = useCallback(() => dispatch({ type: "type", now: Date.now() }), [dispatch]); + const onFocus = useCallback(() => dispatch({ type: "focus" }), [dispatch]); + const onBlur = useCallback(() => dispatch({ type: "blur" }), [dispatch]); + const onSend = useCallback(() => dispatch({ type: "send" }), [dispatch]); + useEffect(() => { + if (state.held.size === 0) return; + const timeout = window.setTimeout( + () => dispatch({ type: "idle" }), + Math.max(0, state.lastTypedAt + COMPOSER_TYPING_IDLE_MS - Date.now()), + ); + return () => window.clearTimeout(timeout); + }, [state.held, state.lastTypedAt, dispatch]); + return { heldRequestIds: state.held, onDraftChange, onFocus, onBlur, onSend }; +} From 08e18b7833c9b5d33bfcd1ce8976dcb67c11dde5 Mon Sep 17 00:00:00 2001 From: Exotic <118054752+extoci@users.noreply.github.com> Date: Sat, 10 Oct 2026 02:47:36 +0300 Subject: [PATCH 32/39] fix(models): remove new badges from Claude Opus and Sonnet 5.5 (#17646) --- apps/server/src/provider/model-manifest.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/server/src/provider/model-manifest.json b/apps/server/src/provider/model-manifest.json index 943dfbb11fe9..6b950ed2d060 100644 --- a/apps/server/src/provider/model-manifest.json +++ b/apps/server/src/provider/model-manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "updatedAt": "2026-10-08T21:30:00Z", + "updatedAt": "2026-10-09T23:31:10Z", "compatibility": [ { "driver": "codex", @@ -668,7 +668,6 @@ "name": "Claude Opus 5.5", "aliases": ["opus", "opus-5.5", "claude-opus-5.5"], "status": "current", - "badge": "new", "profile": "opus-5-5", "adapter": { "claudeCode": { "minVersion": "2.1.280" } } }, @@ -677,7 +676,6 @@ "name": "Claude Sonnet 5.5", "aliases": ["sonnet", "sonnet-5.5", "claude-sonnet-5.5"], "status": "current", - "badge": "new", "profile": "sonnet-5", "adapter": { "claudeCode": { "minVersion": "2.1.284" } } }, From 5dcb59ed6c191589e730fb964aabad005466eb6d Mon Sep 17 00:00:00 2001 From: maria Date: Fri, 9 Oct 2026 20:54:14 -0300 Subject: [PATCH 33/39] fix(ui): keep focus and selection borders visible across the app (#16675) Co-authored-by: Claude Opus 5.5 (1M context) --- .../src/permissions/MacPermissionHelper.ts | 2 +- apps/desktop/src/preview/Annotation.css | 2 +- .../src/preview/AnnotationStyles.generated.ts | 2 +- apps/desktop/src/preview/PickPreload.ts | 52 +++++++- apps/server/src/htmlRender/HtmlRender.test.ts | 5 +- apps/web/src/components/ContextChip.tsx | 4 +- apps/web/src/components/GitActionsControl.tsx | 10 +- apps/web/src/components/LegacySidebar.tsx | 10 +- apps/web/src/components/RightPanelTabs.tsx | 2 +- apps/web/src/components/Sidebar.tsx | 6 +- .../components/auth/ConnectAgentSurface.tsx | 4 +- .../src/components/chat/ChangedFilesTree.tsx | 4 +- apps/web/src/components/chat/ChatComposer.tsx | 6 +- apps/web/src/components/chat/ChatHeader.tsx | 8 +- .../src/components/chat/ComposerControl.tsx | 2 +- .../chat/ComposerPendingUserInputPanel.tsx | 2 +- .../chat/ComposerPrimaryActions.tsx | 4 +- .../chat/ComposerServerUpdateStatus.tsx | 2 +- .../src/components/chat/ComposerStashMenu.tsx | 2 +- .../src/components/chat/DraftHeroHeadline.tsx | 2 +- .../src/components/chat/MessagesTimeline.tsx | 6 +- .../components/chat/ThreadDetailsControl.tsx | 2 +- .../src/components/chat/V2LifecycleRow.tsx | 2 +- .../web/src/components/chat/ZoomableImage.tsx | 2 +- .../src/components/files/FileBreadcrumbs.tsx | 2 +- .../components/files/fileSurfaceChrome.tsx | 2 +- .../src/components/media/MediaVideoPlayer.tsx | 2 +- .../preview/PreviewRecentUrlCard.tsx | 2 +- .../src/components/projectScriptEditor.tsx | 2 +- .../pullRequest/PullRequestCopyableCode.tsx | 2 +- .../pullRequest/PullRequestDetailPanel.tsx | 2 +- .../pullRequest/PullRequestReactions.tsx | 4 +- .../components/pullRequest/PullRequestRow.tsx | 2 +- .../pullRequest/PullRequestTimelineTab.tsx | 2 +- .../settings/AddProviderInstanceDialog.tsx | 2 +- .../settings/BrowserImportWizard.tsx | 2 +- .../components/settings/CodexSetupSection.tsx | 2 +- .../settings/ConnectionsSettings.tsx | 8 +- .../settings/EnvironmentRoutesList.tsx | 2 +- .../settings/FoldedSettingsSection.tsx | 4 +- .../settings/KeybindingsSettings.tsx | 4 +- .../settings/PanelAnimationsPreview.tsx | 2 +- .../settings/ProjectIconPickerDialog.tsx | 6 +- .../settings/ProviderInstanceCard.tsx | 2 +- .../components/settings/ThemeColorPicker.tsx | 4 +- .../components/settings/ThemeImportDialog.tsx | 2 +- .../settings/ThemePreviewCircles.tsx | 2 +- .../src/components/settings/ThemeSettings.tsx | 16 ++- .../components/settings/settingsLayout.tsx | 2 +- .../src/components/sidebar/SidebarChrome.tsx | 2 +- .../sidebar/SidebarProviderUpdatePill.tsx | 2 +- .../components/sidebar/SidebarUpdatePill.tsx | 2 +- .../sidebar/SidebarUpdateReleaseNotes.tsx | 2 +- apps/web/src/components/ui/badge.tsx | 2 +- apps/web/src/components/ui/button.tsx | 2 +- apps/web/src/components/ui/calendar.tsx | 4 +- apps/web/src/components/ui/checkbox.tsx | 5 +- apps/web/src/components/ui/color-picker.tsx | 4 +- apps/web/src/components/ui/input-group.tsx | 2 +- apps/web/src/components/ui/input.tsx | 2 +- apps/web/src/components/ui/menu.tsx | 2 +- apps/web/src/components/ui/number-field.tsx | 2 +- apps/web/src/components/ui/radio-group.tsx | 5 +- apps/web/src/components/ui/scroll-area.tsx | 5 +- apps/web/src/components/ui/select.tsx | 4 +- apps/web/src/components/ui/sidebar.tsx | 4 +- apps/web/src/components/ui/switch.tsx | 2 +- apps/web/src/components/ui/textarea.tsx | 2 +- apps/web/src/components/ui/toast.tsx | 4 +- apps/web/src/components/ui/toggle.tsx | 2 +- apps/web/src/components/ui/wizard.tsx | 4 +- apps/web/src/components/usage/UsageLimits.tsx | 2 +- .../components/usage/UsageLimitsPooled.tsx | 2 +- apps/web/src/index.css | 20 ++- oxlint-plugin-t3code/index.ts | 2 + .../rules/no-outset-state-indicators.ts | 126 ++++++++++++++++++ .../rules/no-unscoped-has.test.ts | 39 ++++++ packages/shared/src/htmlRender.ts | 4 +- vite.config.ts | 5 + 79 files changed, 359 insertions(+), 131 deletions(-) create mode 100644 oxlint-plugin-t3code/rules/no-outset-state-indicators.ts diff --git a/apps/desktop/src/permissions/MacPermissionHelper.ts b/apps/desktop/src/permissions/MacPermissionHelper.ts index c02b338c556b..3f395c81ffc4 100644 --- a/apps/desktop/src/permissions/MacPermissionHelper.ts +++ b/apps/desktop/src/permissions/MacPermissionHelper.ts @@ -52,7 +52,7 @@ body { margin: 0; background: transparent; color: var(--text); font: 15px/22px - #panel { position: relative; margin: 2px; padding: 20px; height: 136px; border: 1px solid var(--line); border-radius: 24px; background: var(--base); } header { font-weight: 600; white-space: nowrap; } button { font: inherit; color: inherit; } -button:focus-visible { outline: 2px solid #007aff; outline-offset: 3px; } +button:focus-visible { outline: 2px solid #007aff; outline-offset: -2px; } #close { position: absolute; right: 8px; top: 6px; width: 22px; height: 22px; padding: 0; border: 0; border-radius: 50%; background: var(--base); font-size: 18px; cursor: pointer; opacity: 0; } #panel:hover #close, #close:focus-visible { opacity: 1; } #app { display: flex; align-items: center; gap: 12px; width: 100%; height: 52px; margin-top: 20px; padding: 8px 12px; border: 0; border-radius: 10px; background: var(--row); cursor: grab; text-align: left; font-size: 16px; font-weight: 600; } diff --git a/apps/desktop/src/preview/Annotation.css b/apps/desktop/src/preview/Annotation.css index 89676a22d585..0897aad4f86d 100644 --- a/apps/desktop/src/preview/Annotation.css +++ b/apps/desktop/src/preview/Annotation.css @@ -64,5 +64,5 @@ input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 2px solid color-mix(in srgb, var(--t3-ring) 72%, transparent); - outline-offset: 1px; + outline-offset: -2px; } diff --git a/apps/desktop/src/preview/AnnotationStyles.generated.ts b/apps/desktop/src/preview/AnnotationStyles.generated.ts index aba581ab5338..ce8dbfb43e6e 100644 --- a/apps/desktop/src/preview/AnnotationStyles.generated.ts +++ b/apps/desktop/src/preview/AnnotationStyles.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/build-preview-annotation-css.mjs. Do not edit. export const previewAnnotationStyles = - '/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n@layer properties;\n:root, :host {\n --spacing: 0.25rem;\n --text-xs: 0.75rem;\n --text-xs--line-height: calc(1 / 0.75);\n --text-sm: 0.875rem;\n --text-sm--line-height: calc(1.25 / 0.875);\n --text-lg: 1.125rem;\n --text-lg--line-height: calc(1.75 / 1.125);\n --font-weight-medium: 500;\n --font-weight-semibold: 600;\n --font-weight-bold: 700;\n --blur-xl: 24px;\n --default-font-family: var(--t3-font-sans);\n --default-mono-font-family: var(--t3-font-mono);\n}\n*, ::after, ::before, ::backdrop, ::file-selector-button {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n border: 0 solid;\n}\nhtml, :host {\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n tab-size: 4;\n font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, \'Helvetica Neue\', \'Noto Sans\', Arial, sans-serif, \'Apple Color Emoji\', \'Segoe UI Emoji\', \'Segoe UI Symbol\', \'Noto Color Emoji\');\n font-feature-settings: var(--default-font-feature-settings, normal);\n font-variation-settings: var(--default-font-variation-settings, normal);\n -webkit-tap-highlight-color: transparent;\n}\nhr {\n height: 0;\n color: inherit;\n border-top-width: 1px;\n}\nabbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n}\nh1, h2, h3, h4, h5, h6 {\n font-size: inherit;\n font-weight: inherit;\n}\na {\n color: inherit;\n -webkit-text-decoration: inherit;\n text-decoration: inherit;\n}\nb, strong {\n font-weight: bolder;\n}\ncode, kbd, samp, pre {\n font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \'Liberation Mono\', \'Courier New\', monospace);\n font-feature-settings: var(--default-mono-font-feature-settings, normal);\n font-variation-settings: var(--default-mono-font-variation-settings, normal);\n font-size: 1em;\n}\nsmall {\n font-size: 80%;\n}\nsub, sup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsub {\n bottom: -0.25em;\n}\nsup {\n top: -0.5em;\n}\ntable {\n text-indent: 0;\n border-color: inherit;\n border-collapse: collapse;\n}\n:-moz-focusring:where(:not(iframe)) {\n outline: auto;\n}\nprogress {\n vertical-align: baseline;\n}\nsummary {\n display: list-item;\n}\nol, ul, menu {\n list-style: none;\n}\nimg, svg, video, canvas, audio, iframe, embed, object {\n display: block;\n vertical-align: middle;\n}\nimg, video {\n max-width: 100%;\n height: auto;\n}\nbutton, input, select, optgroup, textarea, ::file-selector-button {\n font: inherit;\n font-feature-settings: inherit;\n font-variation-settings: inherit;\n letter-spacing: inherit;\n color: inherit;\n border-radius: 0;\n background-color: transparent;\n opacity: 1;\n}\n:where(select:is([multiple], [size])) optgroup {\n font-weight: bolder;\n}\n:where(select:is([multiple], [size])) optgroup option {\n padding-inline-start: 20px;\n}\n::file-selector-button {\n margin-inline-end: 4px;\n}\n::placeholder {\n opacity: 1;\n}\n@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {\n ::placeholder {\n color: currentcolor;\n @supports (color: color-mix(in lab, red, red)) {\n color: color-mix(in oklab, currentcolor 50%, transparent);\n }\n }\n}\ntextarea {\n resize: vertical;\n}\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n::-webkit-date-and-time-value {\n min-height: 1lh;\n text-align: inherit;\n}\n::-webkit-datetime-edit {\n display: inline-flex;\n}\n::-webkit-datetime-edit-fields-wrapper {\n padding: 0;\n}\n::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {\n padding-block: 0;\n}\n::-webkit-calendar-picker-indicator {\n line-height: 1;\n}\n:-moz-ui-invalid {\n box-shadow: none;\n}\nbutton, input:where([type=\'button\'], [type=\'reset\'], [type=\'submit\']), ::file-selector-button {\n appearance: button;\n}\n::-webkit-inner-spin-button, ::-webkit-outer-spin-button {\n height: auto;\n}\n[hidden]:where(:not([hidden=\'until-found\'])) {\n display: none !important;\n}\n.pointer-events-auto {\n pointer-events: auto;\n}\n.pointer-events-none {\n pointer-events: none;\n}\n.absolute {\n position: absolute;\n}\n.fixed {\n position: fixed;\n}\n.inset-0 {\n inset: 0px;\n}\n.top-1\\/2 {\n top: calc(1 / 2 * 100%);\n}\n.top-2\\.5 {\n top: calc(var(--spacing) * 2.5);\n}\n.right-2 {\n right: calc(var(--spacing) * 2);\n}\n.left-1\\/2 {\n left: calc(1 / 2 * 100%);\n}\n.z-1 {\n z-index: 1;\n}\n.block {\n display: block;\n}\n.flex {\n display: flex;\n}\n.grid {\n display: grid;\n}\n.hidden {\n display: none;\n}\n.inline-flex {\n display: inline-flex;\n}\n.h-7 {\n height: calc(var(--spacing) * 7);\n}\n.h-8 {\n height: calc(var(--spacing) * 8);\n}\n.max-h-24 {\n max-height: calc(var(--spacing) * 24);\n}\n.max-h-\\[calc\\(100vh-16px\\)\\] {\n max-height: calc(100vh - 16px);\n}\n.max-h-\\[min\\(176px\\,calc\\(100vh-180px\\)\\)\\] {\n max-height: min(176px, calc(100vh - 180px));\n}\n.min-h-7 {\n min-height: calc(var(--spacing) * 7);\n}\n.min-h-8 {\n min-height: calc(var(--spacing) * 8);\n}\n.w-6 {\n width: calc(var(--spacing) * 6);\n}\n.w-8 {\n width: calc(var(--spacing) * 8);\n}\n.w-\\[min\\(360px\\,calc\\(100vw-16px\\)\\)\\] {\n width: min(360px, calc(100vw - 16px));\n}\n.w-full {\n width: 100%;\n}\n.max-w-70 {\n max-width: calc(var(--spacing) * 70);\n}\n.min-w-0 {\n min-width: 0px;\n}\n.flex-1 {\n flex: 1;\n}\n.shrink-0 {\n flex-shrink: 0;\n}\n.-translate-x-1\\/2 {\n --tw-translate-x: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n}\n.-translate-y-1\\/2 {\n --tw-translate-y: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n}\n.cursor-grab {\n cursor: grab;\n}\n.cursor-pointer {\n cursor: pointer;\n}\n.resize {\n resize: both;\n}\n.resize-none {\n resize: none;\n}\n.appearance-none {\n appearance: none;\n}\n.grid-cols-\\[22px_minmax\\(0\\,1fr\\)\\] {\n grid-template-columns: 22px minmax(0,1fr);\n}\n.grid-cols-\\[82px_minmax\\(0\\,1fr\\)\\] {\n grid-template-columns: 82px minmax(0,1fr);\n}\n.flex-col {\n flex-direction: column;\n}\n.items-center {\n align-items: center;\n}\n.items-start {\n align-items: flex-start;\n}\n.justify-center {\n justify-content: center;\n}\n.gap-0\\.5 {\n gap: calc(var(--spacing) * 0.5);\n}\n.gap-1 {\n gap: var(--spacing);\n}\n.gap-2 {\n gap: calc(var(--spacing) * 2);\n}\n.overflow-auto {\n overflow: auto;\n}\n.overflow-hidden {\n overflow: hidden;\n}\n.overflow-y-hidden {\n overflow-y: hidden;\n}\n.rounded-lg {\n border-radius: var(--t3-radius);\n}\n.rounded-md {\n border-radius: calc(var(--t3-radius) - 2px);\n}\n.rounded-xl {\n border-radius: calc(var(--t3-radius) + 4px);\n}\n.border {\n border-style: var(--tw-border-style);\n border-width: 1px;\n}\n.border-0 {\n border-style: var(--tw-border-style);\n border-width: 0px;\n}\n.border-t {\n border-top-style: var(--tw-border-style);\n border-top-width: 1px;\n}\n.border-b {\n border-bottom-style: var(--tw-border-style);\n border-bottom-width: 1px;\n}\n.border-border {\n border-color: var(--t3-border);\n}\n.border-input {\n border-color: var(--t3-input);\n}\n.border-primary {\n border-color: var(--t3-primary);\n}\n.border-transparent {\n border-color: transparent;\n}\n.border-b-transparent {\n border-bottom-color: transparent;\n}\n.bg-background {\n background-color: var(--t3-background);\n}\n.bg-muted {\n background-color: var(--t3-muted);\n}\n.bg-muted\\/40 {\n background-color: var(--t3-muted);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--t3-muted) 40%, transparent);\n }\n}\n.bg-popover\\/95 {\n background-color: var(--t3-popover);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--t3-popover) 95%, transparent);\n }\n}\n.bg-popover\\/96 {\n background-color: var(--t3-popover);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--t3-popover) 96%, transparent);\n }\n}\n.bg-primary {\n background-color: var(--t3-primary);\n}\n.bg-primary\\/10 {\n background-color: var(--t3-primary);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--t3-primary) 10%, transparent);\n }\n}\n.bg-transparent {\n background-color: transparent;\n}\n.p-0 {\n padding: 0px;\n}\n.p-1 {\n padding: var(--spacing);\n}\n.p-2 {\n padding: calc(var(--spacing) * 2);\n}\n.px-0 {\n padding-inline: 0px;\n}\n.px-1 {\n padding-inline: var(--spacing);\n}\n.px-2 {\n padding-inline: calc(var(--spacing) * 2);\n}\n.px-2\\.5 {\n padding-inline: calc(var(--spacing) * 2.5);\n}\n.px-3 {\n padding-inline: calc(var(--spacing) * 3);\n}\n.py-1 {\n padding-block: var(--spacing);\n}\n.py-1\\.5 {\n padding-block: calc(var(--spacing) * 1.5);\n}\n.py-2 {\n padding-block: calc(var(--spacing) * 2);\n}\n.font-mono {\n font-family: var(--t3-font-mono);\n}\n.font-sans {\n font-family: var(--t3-font-sans);\n}\n.text-lg {\n font-size: var(--text-lg);\n line-height: var(--tw-leading, var(--text-lg--line-height));\n}\n.text-sm {\n font-size: var(--text-sm);\n line-height: var(--tw-leading, var(--text-sm--line-height));\n}\n.text-xs {\n font-size: var(--text-xs);\n line-height: var(--tw-leading, var(--text-xs--line-height));\n}\n.leading-5 {\n --tw-leading: calc(var(--spacing) * 5);\n line-height: calc(var(--spacing) * 5);\n}\n.font-bold {\n --tw-font-weight: var(--font-weight-bold);\n font-weight: var(--font-weight-bold);\n}\n.font-medium {\n --tw-font-weight: var(--font-weight-medium);\n font-weight: var(--font-weight-medium);\n}\n.font-semibold {\n --tw-font-weight: var(--font-weight-semibold);\n font-weight: var(--font-weight-semibold);\n}\n.text-foreground {\n color: var(--t3-foreground);\n}\n.text-muted-foreground {\n color: var(--t3-muted-foreground);\n}\n.text-popover-foreground {\n color: var(--t3-popover-foreground);\n}\n.text-primary {\n color: var(--t3-primary);\n}\n.text-primary-foreground {\n color: var(--t3-primary-foreground);\n}\n.shadow-2xl {\n --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.shadow-lg {\n --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.shadow-md {\n --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.shadow-sm {\n --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.shadow-xs {\n --tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.ring-0 {\n --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.blur {\n --tw-blur: blur(8px);\n filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);\n}\n.backdrop-blur-xl {\n --tw-backdrop-blur: blur(var(--blur-xl));\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n}\n.outline-none {\n --tw-outline-style: none;\n outline-style: none;\n}\n.select-none {\n -webkit-user-select: none;\n user-select: none;\n}\n.placeholder\\:text-muted-foreground::placeholder {\n color: var(--t3-muted-foreground);\n}\n@media (hover: hover) {\n .hover\\:bg-accent:hover {\n background-color: var(--t3-accent);\n }\n .hover\\:bg-primary\\/90:hover {\n background-color: var(--t3-primary);\n }\n @supports (color: color-mix(in lab, red, red)) {\n .hover\\:bg-primary\\/90:hover {\n background-color: color-mix(in oklab, var(--t3-primary) 90%, transparent);\n }\n }\n .hover\\:text-accent-foreground:hover {\n color: var(--t3-accent-foreground);\n }\n}\n.focus\\:border-b-primary:focus {\n border-bottom-color: var(--t3-primary);\n}\n.focus\\:ring-0:focus {\n --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.focus\\:outline-none:focus {\n --tw-outline-style: none;\n outline-style: none;\n}\n.disabled\\:pointer-events-none:disabled {\n pointer-events: none;\n}\n.disabled\\:opacity-60:disabled {\n opacity: 60%;\n}\n:host {\n --t3-font-sans: "DM Sans Variable", "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui,\n sans-serif;\n --t3-font-mono: "SF Mono", "SFMono-Regular", "JetBrains Mono", Consolas, "Liberation Mono", Menlo, monospace;\n --t3-radius: 0.625rem;\n --t3-background: white;\n --t3-foreground: oklch(0.269 0 0);\n --t3-popover: white;\n --t3-popover-foreground: oklch(0.269 0 0);\n --t3-primary: oklch(0.488 0.217 264);\n --t3-primary-foreground: white;\n --t3-muted: rgb(0 0 0 / 4%);\n --t3-muted-foreground: oklch(0.556 0 0);\n --t3-accent: rgb(0 0 0 / 4%);\n --t3-accent-foreground: oklch(0.269 0 0);\n --t3-border: rgb(0 0 0 / 8%);\n --t3-input: rgb(0 0 0 / 10%);\n --t3-ring: oklch(0.488 0.217 264);\n color: var(--t3-foreground);\n font-family: var(--t3-font-sans);\n}\n* {\n box-sizing: border-box;\n border-color: var(--t3-border);\n}\nbutton, input, select, textarea {\n font: inherit;\n}\nbutton:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible {\n outline: 2px solid var(--t3-ring);\n @supports (color: color-mix(in lab, red, red)) {\n outline: 2px solid color-mix(in srgb, var(--t3-ring) 72%, transparent);\n }\n outline-offset: 1px;\n}\n@property --tw-translate-x {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-y {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-z {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-border-style {\n syntax: "*";\n inherits: false;\n initial-value: solid;\n}\n@property --tw-leading {\n syntax: "*";\n inherits: false;\n}\n@property --tw-font-weight {\n syntax: "*";\n inherits: false;\n}\n@property --tw-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-shadow-alpha {\n syntax: "";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-inset-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-inset-shadow-alpha {\n syntax: "";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-ring-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-ring-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-ring-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-inset-ring-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-ring-inset {\n syntax: "*";\n inherits: false;\n}\n@property --tw-ring-offset-width {\n syntax: "";\n inherits: false;\n initial-value: 0px;\n}\n@property --tw-ring-offset-color {\n syntax: "*";\n inherits: false;\n initial-value: #fff;\n}\n@property --tw-ring-offset-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-blur {\n syntax: "*";\n inherits: false;\n}\n@property --tw-brightness {\n syntax: "*";\n inherits: false;\n}\n@property --tw-contrast {\n syntax: "*";\n inherits: false;\n}\n@property --tw-grayscale {\n syntax: "*";\n inherits: false;\n}\n@property --tw-hue-rotate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-invert {\n syntax: "*";\n inherits: false;\n}\n@property --tw-opacity {\n syntax: "*";\n inherits: false;\n}\n@property --tw-saturate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-sepia {\n syntax: "*";\n inherits: false;\n}\n@property --tw-drop-shadow {\n syntax: "*";\n inherits: false;\n}\n@property --tw-drop-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-drop-shadow-alpha {\n syntax: "";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-drop-shadow-size {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-blur {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-brightness {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-contrast {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-grayscale {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-hue-rotate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-invert {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-opacity {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-saturate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-sepia {\n syntax: "*";\n inherits: false;\n}\n@layer properties {\n @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {\n *, ::before, ::after, ::backdrop {\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-translate-z: 0;\n --tw-border-style: solid;\n --tw-leading: initial;\n --tw-font-weight: initial;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-color: initial;\n --tw-shadow-alpha: 100%;\n --tw-inset-shadow: 0 0 #0000;\n --tw-inset-shadow-color: initial;\n --tw-inset-shadow-alpha: 100%;\n --tw-ring-color: initial;\n --tw-ring-shadow: 0 0 #0000;\n --tw-inset-ring-color: initial;\n --tw-inset-ring-shadow: 0 0 #0000;\n --tw-ring-inset: initial;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-blur: initial;\n --tw-brightness: initial;\n --tw-contrast: initial;\n --tw-grayscale: initial;\n --tw-hue-rotate: initial;\n --tw-invert: initial;\n --tw-opacity: initial;\n --tw-saturate: initial;\n --tw-sepia: initial;\n --tw-drop-shadow: initial;\n --tw-drop-shadow-color: initial;\n --tw-drop-shadow-alpha: 100%;\n --tw-drop-shadow-size: initial;\n --tw-backdrop-blur: initial;\n --tw-backdrop-brightness: initial;\n --tw-backdrop-contrast: initial;\n --tw-backdrop-grayscale: initial;\n --tw-backdrop-hue-rotate: initial;\n --tw-backdrop-invert: initial;\n --tw-backdrop-opacity: initial;\n --tw-backdrop-saturate: initial;\n --tw-backdrop-sepia: initial;\n }\n }\n}\n'; + '/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n@layer properties;\n:root, :host {\n --spacing: 0.25rem;\n --text-xs: 0.75rem;\n --text-xs--line-height: calc(1 / 0.75);\n --text-sm: 0.875rem;\n --text-sm--line-height: calc(1.25 / 0.875);\n --text-lg: 1.125rem;\n --text-lg--line-height: calc(1.75 / 1.125);\n --font-weight-medium: 500;\n --font-weight-semibold: 600;\n --font-weight-bold: 700;\n --blur-xl: 24px;\n --default-font-family: var(--t3-font-sans);\n --default-mono-font-family: var(--t3-font-mono);\n}\n*, ::after, ::before, ::backdrop, ::file-selector-button {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n border: 0 solid;\n}\nhtml, :host {\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n tab-size: 4;\n font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, \'Helvetica Neue\', \'Noto Sans\', Arial, sans-serif, \'Apple Color Emoji\', \'Segoe UI Emoji\', \'Segoe UI Symbol\', \'Noto Color Emoji\');\n font-feature-settings: var(--default-font-feature-settings, normal);\n font-variation-settings: var(--default-font-variation-settings, normal);\n -webkit-tap-highlight-color: transparent;\n}\nhr {\n height: 0;\n color: inherit;\n border-top-width: 1px;\n}\nabbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n}\nh1, h2, h3, h4, h5, h6 {\n font-size: inherit;\n font-weight: inherit;\n}\na {\n color: inherit;\n -webkit-text-decoration: inherit;\n text-decoration: inherit;\n}\nb, strong {\n font-weight: bolder;\n}\ncode, kbd, samp, pre {\n font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \'Liberation Mono\', \'Courier New\', monospace);\n font-feature-settings: var(--default-mono-font-feature-settings, normal);\n font-variation-settings: var(--default-mono-font-variation-settings, normal);\n font-size: 1em;\n}\nsmall {\n font-size: 80%;\n}\nsub, sup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsub {\n bottom: -0.25em;\n}\nsup {\n top: -0.5em;\n}\ntable {\n text-indent: 0;\n border-color: inherit;\n border-collapse: collapse;\n}\n:-moz-focusring:where(:not(iframe)) {\n outline: auto;\n}\nprogress {\n vertical-align: baseline;\n}\nsummary {\n display: list-item;\n}\nol, ul, menu {\n list-style: none;\n}\nimg, svg, video, canvas, audio, iframe, embed, object {\n display: block;\n vertical-align: middle;\n}\nimg, video {\n max-width: 100%;\n height: auto;\n}\nbutton, input, select, optgroup, textarea, ::file-selector-button {\n font: inherit;\n font-feature-settings: inherit;\n font-variation-settings: inherit;\n letter-spacing: inherit;\n color: inherit;\n border-radius: 0;\n background-color: transparent;\n opacity: 1;\n}\n:where(select:is([multiple], [size])) optgroup {\n font-weight: bolder;\n}\n:where(select:is([multiple], [size])) optgroup option {\n padding-inline-start: 20px;\n}\n::file-selector-button {\n margin-inline-end: 4px;\n}\n::placeholder {\n opacity: 1;\n}\n@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {\n ::placeholder {\n color: currentcolor;\n @supports (color: color-mix(in lab, red, red)) {\n color: color-mix(in oklab, currentcolor 50%, transparent);\n }\n }\n}\ntextarea {\n resize: vertical;\n}\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n::-webkit-date-and-time-value {\n min-height: 1lh;\n text-align: inherit;\n}\n::-webkit-datetime-edit {\n display: inline-flex;\n}\n::-webkit-datetime-edit-fields-wrapper {\n padding: 0;\n}\n::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {\n padding-block: 0;\n}\n::-webkit-calendar-picker-indicator {\n line-height: 1;\n}\n:-moz-ui-invalid {\n box-shadow: none;\n}\nbutton, input:where([type=\'button\'], [type=\'reset\'], [type=\'submit\']), ::file-selector-button {\n appearance: button;\n}\n::-webkit-inner-spin-button, ::-webkit-outer-spin-button {\n height: auto;\n}\n[hidden]:where(:not([hidden=\'until-found\'])) {\n display: none !important;\n}\n.pointer-events-auto {\n pointer-events: auto;\n}\n.pointer-events-none {\n pointer-events: none;\n}\n.visible {\n visibility: visible;\n}\n.absolute {\n position: absolute;\n}\n.fixed {\n position: fixed;\n}\n.static {\n position: static;\n}\n.inset-0 {\n inset: 0px;\n}\n.top-1\\/2 {\n top: calc(1 / 2 * 100%);\n}\n.top-2\\.5 {\n top: calc(var(--spacing) * 2.5);\n}\n.right-2 {\n right: calc(var(--spacing) * 2);\n}\n.left-1\\/2 {\n left: calc(1 / 2 * 100%);\n}\n.z-1 {\n z-index: 1;\n}\n.block {\n display: block;\n}\n.flex {\n display: flex;\n}\n.grid {\n display: grid;\n}\n.hidden {\n display: none;\n}\n.inline-flex {\n display: inline-flex;\n}\n.h-7 {\n height: calc(var(--spacing) * 7);\n}\n.h-8 {\n height: calc(var(--spacing) * 8);\n}\n.max-h-24 {\n max-height: calc(var(--spacing) * 24);\n}\n.max-h-\\[calc\\(100vh-16px\\)\\] {\n max-height: calc(100vh - 16px);\n}\n.max-h-\\[min\\(176px\\,calc\\(100vh-180px\\)\\)\\] {\n max-height: min(176px, calc(100vh - 180px));\n}\n.min-h-7 {\n min-height: calc(var(--spacing) * 7);\n}\n.min-h-8 {\n min-height: calc(var(--spacing) * 8);\n}\n.w-6 {\n width: calc(var(--spacing) * 6);\n}\n.w-8 {\n width: calc(var(--spacing) * 8);\n}\n.w-\\[min\\(360px\\,calc\\(100vw-16px\\)\\)\\] {\n width: min(360px, calc(100vw - 16px));\n}\n.w-full {\n width: 100%;\n}\n.max-w-70 {\n max-width: calc(var(--spacing) * 70);\n}\n.min-w-0 {\n min-width: 0px;\n}\n.flex-1 {\n flex: 1;\n}\n.shrink-0 {\n flex-shrink: 0;\n}\n.-translate-x-1\\/2 {\n --tw-translate-x: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n}\n.-translate-y-1\\/2 {\n --tw-translate-y: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n}\n.cursor-grab {\n cursor: grab;\n}\n.cursor-pointer {\n cursor: pointer;\n}\n.resize {\n resize: both;\n}\n.resize-none {\n resize: none;\n}\n.appearance-none {\n appearance: none;\n}\n.grid-cols-\\[22px_minmax\\(0\\,1fr\\)\\] {\n grid-template-columns: 22px minmax(0,1fr);\n}\n.grid-cols-\\[82px_minmax\\(0\\,1fr\\)\\] {\n grid-template-columns: 82px minmax(0,1fr);\n}\n.flex-col {\n flex-direction: column;\n}\n.items-center {\n align-items: center;\n}\n.items-start {\n align-items: flex-start;\n}\n.justify-center {\n justify-content: center;\n}\n.gap-0\\.5 {\n gap: calc(var(--spacing) * 0.5);\n}\n.gap-1 {\n gap: var(--spacing);\n}\n.gap-2 {\n gap: calc(var(--spacing) * 2);\n}\n.overflow-auto {\n overflow: auto;\n}\n.overflow-hidden {\n overflow: hidden;\n}\n.overflow-y-hidden {\n overflow-y: hidden;\n}\n.rounded-lg {\n border-radius: var(--t3-radius);\n}\n.rounded-md {\n border-radius: calc(var(--t3-radius) - 2px);\n}\n.rounded-xl {\n border-radius: calc(var(--t3-radius) + 4px);\n}\n.border {\n border-style: var(--tw-border-style);\n border-width: 1px;\n}\n.border-0 {\n border-style: var(--tw-border-style);\n border-width: 0px;\n}\n.border-t {\n border-top-style: var(--tw-border-style);\n border-top-width: 1px;\n}\n.border-b {\n border-bottom-style: var(--tw-border-style);\n border-bottom-width: 1px;\n}\n.border-border {\n border-color: var(--t3-border);\n}\n.border-input {\n border-color: var(--t3-input);\n}\n.border-primary {\n border-color: var(--t3-primary);\n}\n.border-transparent {\n border-color: transparent;\n}\n.border-b-transparent {\n border-bottom-color: transparent;\n}\n.bg-background {\n background-color: var(--t3-background);\n}\n.bg-muted {\n background-color: var(--t3-muted);\n}\n.bg-muted\\/40 {\n background-color: var(--t3-muted);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--t3-muted) 40%, transparent);\n }\n}\n.bg-popover\\/95 {\n background-color: var(--t3-popover);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--t3-popover) 95%, transparent);\n }\n}\n.bg-popover\\/96 {\n background-color: var(--t3-popover);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--t3-popover) 96%, transparent);\n }\n}\n.bg-primary {\n background-color: var(--t3-primary);\n}\n.bg-primary\\/10 {\n background-color: var(--t3-primary);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--t3-primary) 10%, transparent);\n }\n}\n.bg-transparent {\n background-color: transparent;\n}\n.p-0 {\n padding: 0px;\n}\n.p-1 {\n padding: var(--spacing);\n}\n.p-2 {\n padding: calc(var(--spacing) * 2);\n}\n.px-0 {\n padding-inline: 0px;\n}\n.px-1 {\n padding-inline: var(--spacing);\n}\n.px-2 {\n padding-inline: calc(var(--spacing) * 2);\n}\n.px-2\\.5 {\n padding-inline: calc(var(--spacing) * 2.5);\n}\n.px-3 {\n padding-inline: calc(var(--spacing) * 3);\n}\n.py-1 {\n padding-block: var(--spacing);\n}\n.py-1\\.5 {\n padding-block: calc(var(--spacing) * 1.5);\n}\n.py-2 {\n padding-block: calc(var(--spacing) * 2);\n}\n.font-mono {\n font-family: var(--t3-font-mono);\n}\n.font-sans {\n font-family: var(--t3-font-sans);\n}\n.text-lg {\n font-size: var(--text-lg);\n line-height: var(--tw-leading, var(--text-lg--line-height));\n}\n.text-sm {\n font-size: var(--text-sm);\n line-height: var(--tw-leading, var(--text-sm--line-height));\n}\n.text-xs {\n font-size: var(--text-xs);\n line-height: var(--tw-leading, var(--text-xs--line-height));\n}\n.leading-5 {\n --tw-leading: calc(var(--spacing) * 5);\n line-height: calc(var(--spacing) * 5);\n}\n.font-bold {\n --tw-font-weight: var(--font-weight-bold);\n font-weight: var(--font-weight-bold);\n}\n.font-medium {\n --tw-font-weight: var(--font-weight-medium);\n font-weight: var(--font-weight-medium);\n}\n.font-semibold {\n --tw-font-weight: var(--font-weight-semibold);\n font-weight: var(--font-weight-semibold);\n}\n.text-foreground {\n color: var(--t3-foreground);\n}\n.text-muted-foreground {\n color: var(--t3-muted-foreground);\n}\n.text-popover-foreground {\n color: var(--t3-popover-foreground);\n}\n.text-primary {\n color: var(--t3-primary);\n}\n.text-primary-foreground {\n color: var(--t3-primary-foreground);\n}\n.shadow-2xl {\n --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.shadow-lg {\n --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.shadow-md {\n --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.shadow-sm {\n --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.shadow-xs {\n --tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.ring-0 {\n --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.blur {\n --tw-blur: blur(8px);\n filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);\n}\n.backdrop-blur-xl {\n --tw-backdrop-blur: blur(var(--blur-xl));\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n}\n.outline-none {\n --tw-outline-style: none;\n outline-style: none;\n}\n.select-none {\n -webkit-user-select: none;\n user-select: none;\n}\n.placeholder\\:text-muted-foreground::placeholder {\n color: var(--t3-muted-foreground);\n}\n@media (hover: hover) {\n .hover\\:bg-accent:hover {\n background-color: var(--t3-accent);\n }\n .hover\\:bg-primary\\/90:hover {\n background-color: var(--t3-primary);\n }\n @supports (color: color-mix(in lab, red, red)) {\n .hover\\:bg-primary\\/90:hover {\n background-color: color-mix(in oklab, var(--t3-primary) 90%, transparent);\n }\n }\n .hover\\:text-accent-foreground:hover {\n color: var(--t3-accent-foreground);\n }\n}\n.focus\\:border-b-primary:focus {\n border-bottom-color: var(--t3-primary);\n}\n.focus\\:ring-0:focus {\n --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n}\n.focus\\:outline-none:focus {\n --tw-outline-style: none;\n outline-style: none;\n}\n.disabled\\:pointer-events-none:disabled {\n pointer-events: none;\n}\n.disabled\\:opacity-60:disabled {\n opacity: 60%;\n}\n:host {\n --t3-font-sans: "DM Sans Variable", "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui,\n sans-serif;\n --t3-font-mono: "SF Mono", "SFMono-Regular", "JetBrains Mono", Consolas, "Liberation Mono", Menlo, monospace;\n --t3-radius: 0.625rem;\n --t3-background: white;\n --t3-foreground: oklch(0.269 0 0);\n --t3-popover: white;\n --t3-popover-foreground: oklch(0.269 0 0);\n --t3-primary: oklch(0.488 0.217 264);\n --t3-primary-foreground: white;\n --t3-muted: rgb(0 0 0 / 4%);\n --t3-muted-foreground: oklch(0.556 0 0);\n --t3-accent: rgb(0 0 0 / 4%);\n --t3-accent-foreground: oklch(0.269 0 0);\n --t3-border: rgb(0 0 0 / 8%);\n --t3-input: rgb(0 0 0 / 10%);\n --t3-ring: oklch(0.488 0.217 264);\n color: var(--t3-foreground);\n font-family: var(--t3-font-sans);\n}\n* {\n box-sizing: border-box;\n border-color: var(--t3-border);\n}\nbutton, input, select, textarea {\n font: inherit;\n}\nbutton:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible {\n outline: 2px solid var(--t3-ring);\n @supports (color: color-mix(in lab, red, red)) {\n outline: 2px solid color-mix(in srgb, var(--t3-ring) 72%, transparent);\n }\n outline-offset: -2px;\n}\n@property --tw-translate-x {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-y {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-z {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-border-style {\n syntax: "*";\n inherits: false;\n initial-value: solid;\n}\n@property --tw-leading {\n syntax: "*";\n inherits: false;\n}\n@property --tw-font-weight {\n syntax: "*";\n inherits: false;\n}\n@property --tw-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-shadow-alpha {\n syntax: "";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-inset-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-inset-shadow-alpha {\n syntax: "";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-ring-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-ring-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-ring-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-inset-ring-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-ring-inset {\n syntax: "*";\n inherits: false;\n}\n@property --tw-ring-offset-width {\n syntax: "";\n inherits: false;\n initial-value: 0px;\n}\n@property --tw-ring-offset-color {\n syntax: "*";\n inherits: false;\n initial-value: #fff;\n}\n@property --tw-ring-offset-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-blur {\n syntax: "*";\n inherits: false;\n}\n@property --tw-brightness {\n syntax: "*";\n inherits: false;\n}\n@property --tw-contrast {\n syntax: "*";\n inherits: false;\n}\n@property --tw-grayscale {\n syntax: "*";\n inherits: false;\n}\n@property --tw-hue-rotate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-invert {\n syntax: "*";\n inherits: false;\n}\n@property --tw-opacity {\n syntax: "*";\n inherits: false;\n}\n@property --tw-saturate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-sepia {\n syntax: "*";\n inherits: false;\n}\n@property --tw-drop-shadow {\n syntax: "*";\n inherits: false;\n}\n@property --tw-drop-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-drop-shadow-alpha {\n syntax: "";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-drop-shadow-size {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-blur {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-brightness {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-contrast {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-grayscale {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-hue-rotate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-invert {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-opacity {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-saturate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-sepia {\n syntax: "*";\n inherits: false;\n}\n@layer properties {\n @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {\n *, ::before, ::after, ::backdrop {\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-translate-z: 0;\n --tw-border-style: solid;\n --tw-leading: initial;\n --tw-font-weight: initial;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-color: initial;\n --tw-shadow-alpha: 100%;\n --tw-inset-shadow: 0 0 #0000;\n --tw-inset-shadow-color: initial;\n --tw-inset-shadow-alpha: 100%;\n --tw-ring-color: initial;\n --tw-ring-shadow: 0 0 #0000;\n --tw-inset-ring-color: initial;\n --tw-inset-ring-shadow: 0 0 #0000;\n --tw-ring-inset: initial;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-blur: initial;\n --tw-brightness: initial;\n --tw-contrast: initial;\n --tw-grayscale: initial;\n --tw-hue-rotate: initial;\n --tw-invert: initial;\n --tw-opacity: initial;\n --tw-saturate: initial;\n --tw-sepia: initial;\n --tw-drop-shadow: initial;\n --tw-drop-shadow-color: initial;\n --tw-drop-shadow-alpha: 100%;\n --tw-drop-shadow-size: initial;\n --tw-backdrop-blur: initial;\n --tw-backdrop-brightness: initial;\n --tw-backdrop-contrast: initial;\n --tw-backdrop-grayscale: initial;\n --tw-backdrop-hue-rotate: initial;\n --tw-backdrop-invert: initial;\n --tw-backdrop-opacity: initial;\n --tw-backdrop-saturate: initial;\n --tw-backdrop-sepia: initial;\n }\n }\n}\n'; diff --git a/apps/desktop/src/preview/PickPreload.ts b/apps/desktop/src/preview/PickPreload.ts index a656070f87b4..37c7b99dcb53 100644 --- a/apps/desktop/src/preview/PickPreload.ts +++ b/apps/desktop/src/preview/PickPreload.ts @@ -316,12 +316,43 @@ function createBox(color: string, fill: string): HTMLDivElement { } function positionBox(node: HTMLElement, rect: PreviewAnnotationRect): void { + if (rect.width <= 0 || rect.height <= 0) { + node.style.display = "none"; + return; + } node.style.display = "block"; node.style.transform = `translate(${rect.x}px, ${rect.y}px)`; node.style.width = `${rect.width}px`; node.style.height = `${rect.height}px`; } +/** Clamps a box to the viewport and clipping ancestors, so its border is never painted off-screen. */ +function visibleElementRect(element: Element): PreviewAnnotationRect { + const rect = element.getBoundingClientRect(); + let left = Math.max(0, rect.left); + let top = Math.max(0, rect.top); + let right = Math.min(document.documentElement.clientWidth, rect.right); + let bottom = Math.min(document.documentElement.clientHeight, rect.bottom); + if (getComputedStyle(element).position !== "fixed") { + for (let ancestor = element.parentElement; ancestor; ancestor = ancestor.parentElement) { + if (ancestor === document.body || ancestor === document.documentElement) break; + const style = getComputedStyle(ancestor); + const bounds = ancestor.getBoundingClientRect(); + const clipLeft = bounds.left + ancestor.clientLeft; + const clipTop = bounds.top + ancestor.clientTop; + if (style.overflowX !== "visible") { + left = Math.max(left, clipLeft); + right = Math.min(right, clipLeft + ancestor.clientWidth); + } + if (style.overflowY !== "visible") { + top = Math.max(top, clipTop); + bottom = Math.min(bottom, clipTop + ancestor.clientHeight); + } + } + } + return { x: left, y: top, width: Math.max(0, right - left), height: Math.max(0, bottom - top) }; +} + function createLabel(): HTMLDivElement { const label = document.createElement("div"); label.setAttribute(OVERLAY_ATTRIBUTE, ""); @@ -343,11 +374,15 @@ function updateSelectedVisual(target: SelectedElement): void { target.label.style.display = "none"; return; } - const rect = target.element.getBoundingClientRect(); - positionBox(target.outline, rectFromDomRect(rect)); + const rect = visibleElementRect(target.element); + positionBox(target.outline, rect); + if (rect.width === 0 || rect.height === 0) { + target.label.style.display = "none"; + return; + } target.label.textContent = describeRawElement(target.element); target.label.style.display = "block"; - target.label.style.transform = `translate(${Math.max(4, rect.left)}px, ${Math.max(4, rect.top - 22)}px)`; + target.label.style.transform = `translate(${Math.max(4, rect.x)}px, ${Math.max(4, rect.y - 22)}px)`; } function toStackFrame(frame: { @@ -995,8 +1030,9 @@ function startAnnotation(sendEnabled: boolean): void { const getAnnotationBounds = (): PreviewAnnotationRect | null => unionRects( [ - ...Array.from(selected.values(), (target) => - rectFromDomRect(target.element.getBoundingClientRect()), + // Follow the visible outline, so a clipped selection does not anchor the editor off-screen. + ...Array.from(selected.values(), (target) => visibleElementRect(target.element)).filter( + (rect) => rect.width > 0 && rect.height > 0, ), ...regions.map((region) => region.rect), ...strokes.map((stroke) => stroke.bounds), @@ -1108,6 +1144,8 @@ function startAnnotation(sendEnabled: boolean): void { for (const target of selected.values()) updateSelectedVisual(target); queueEditorLayout(); }; + // A scrollbar appearing narrows the viewport without a window resize. + const rootResizeObserver = new ResizeObserver(repaint); const removeTargetAtPoint = (x: number, y: number): boolean => { for (const target of Array.from(selected.values()).toReversed()) { @@ -1192,7 +1230,7 @@ function startAnnotation(sendEnabled: boolean): void { } if (tool === "select" && dragStart === null) { const target = pickFromPoint(event.clientX, event.clientY); - if (target) positionBox(hoverOutline, rectFromDomRect(target.getBoundingClientRect())); + if (target) positionBox(hoverOutline, visibleElementRect(target)); else clearHoverOutline(); return; } @@ -1310,6 +1348,7 @@ function startAnnotation(sendEnabled: boolean): void { const teardown = (notifyMain: boolean): void => { if (finished) return; finished = true; + rootResizeObserver.disconnect(); restoreStyles(); window.removeEventListener("pointermove", onPointerMove, true); window.removeEventListener("pointerdown", onPointerDown, true); @@ -1445,6 +1484,7 @@ function startAnnotation(sendEnabled: boolean): void { ipcRenderer.on(CANCEL_PICK_CHANNEL, onCancel); ipcRenderer.on(ANNOTATION_CAPTURED_CHANNEL, onCaptured); document.documentElement.appendChild(host); + rootResizeObserver.observe(document.documentElement); refreshToolButtons(); updateStatus(); activeSession = { diff --git a/apps/server/src/htmlRender/HtmlRender.test.ts b/apps/server/src/htmlRender/HtmlRender.test.ts index 930fe11c0a8c..aab68c7b1877 100644 --- a/apps/server/src/htmlRender/HtmlRender.test.ts +++ b/apps/server/src/htmlRender/HtmlRender.test.ts @@ -244,10 +244,12 @@ describe("HtmlRender", () => { const htmlRender = yield* HtmlRender.HtmlRender; const preview = yield* htmlRender.preview({ html: [ - '
', + '
', '', "", @@ -271,6 +273,7 @@ describe("HtmlRender", () => { expect(preview.consoleMessages).toEqual( expect.arrayContaining([ { level: "log", text: "ready 3" }, + { level: "log", text: "focus 2px -2px 400 400" }, { level: "info", text: HTML_RENDER_MEASURE_FONTS.sans }, { level: "warning", diff --git a/apps/web/src/components/ContextChip.tsx b/apps/web/src/components/ContextChip.tsx index d34a450f6650..fdb50a6bb063 100644 --- a/apps/web/src/components/ContextChip.tsx +++ b/apps/web/src/components/ContextChip.tsx @@ -23,7 +23,7 @@ import { cn } from "~/lib/utils"; * span with tabIndex for a tooltip) gets the focus outline. */ const contextChipVariants = cva( - "inline-flex h-[1.41em] max-w-full items-center gap-[0.33em] rounded-[0.5em] border px-[0.5em] align-middle font-medium text-[0.86em] leading-none [&_svg]:block [&_svg]:size-[1.17em] [&_svg]:shrink-0 [&_svg]:self-center [button&,a&,[data-popup-open]&]:cursor-pointer [button&,a&]:transition-colors [button&,a&]:motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground disabled:cursor-default", + "inline-flex h-[1.41em] max-w-full items-center gap-[0.33em] rounded-[0.5em] border px-[0.5em] align-middle font-medium text-[0.86em] leading-none [&_svg]:block [&_svg]:size-[1.17em] [&_svg]:shrink-0 [&_svg]:self-center [button&,a&,[data-popup-open]&]:cursor-pointer [button&,a&]:transition-colors [button&,a&]:motion-reduce:transition-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-foreground disabled:cursor-default", { defaultVariants: { kind: "neutral" }, variants: { @@ -117,7 +117,7 @@ function ContextChipLabel({ className, ...props }: React.ComponentProps<"span">) function ContextChipAction({ className, render, ...props }: useRender.ComponentProps<"button">) { const defaultProps = { className: cn( - "ml-[0.17em] inline-flex size-[1.17em] shrink-0 cursor-pointer items-center justify-center rounded-sm text-current transition-colors hover:bg-(--context-chip-accent,var(--color-foreground))/17 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring motion-reduce:transition-none [&_svg]:size-[0.85em]", + "ml-[0.17em] inline-flex size-[1.17em] shrink-0 cursor-pointer items-center justify-center rounded-sm text-current transition-colors hover:bg-(--context-chip-accent,var(--color-foreground))/17 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring motion-reduce:transition-none [&_svg]:size-[0.85em]", className, ), "data-slot": "context-chip-action", diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index 6e3aff08484e..5615c64ee450 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -793,9 +793,9 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { value={option.value} className={cn( "relative flex cursor-pointer items-center gap-3 rounded-lg border px-3 py-3 text-left outline-none transition-[background-color,border-color,box-shadow]", - "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background", + "focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background", isSelected - ? "border-primary bg-background shadow-sm ring-2 ring-primary/35 dark:border-transparent dark:bg-primary/10 dark:shadow-none dark:ring-1 dark:ring-primary/30" + ? "border-primary bg-background shadow-sm ring-2 ring-inset ring-primary/35 dark:border-transparent dark:bg-primary/10 dark:shadow-none dark:ring-1 dark:ring-primary/30" : "border-border bg-background hover:border-foreground/20 hover:bg-muted/50 dark:border-transparent dark:bg-white/[0.035] dark:hover:bg-accent", )} > @@ -817,7 +817,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { > Repository -
+
{publishHost}/ @@ -879,9 +879,9 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { value={option.value} className={cn( "relative flex cursor-pointer items-center gap-3 rounded-lg border px-3 py-2.5 text-left outline-none transition-[background-color,border-color,box-shadow]", - "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background", + "focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background", isSelected - ? "border-primary bg-background shadow-sm ring-2 ring-primary/35 dark:border-transparent dark:bg-primary/10 dark:shadow-none dark:ring-1 dark:ring-primary/30" + ? "border-primary bg-background shadow-sm ring-2 ring-inset ring-primary/35 dark:border-transparent dark:bg-primary/10 dark:shadow-none dark:ring-1 dark:ring-primary/30" : "border-border bg-background hover:border-foreground/20 hover:bg-muted/50 dark:border-transparent dark:bg-white/[0.035] dark:hover:bg-accent", )} > diff --git a/apps/web/src/components/LegacySidebar.tsx b/apps/web/src/components/LegacySidebar.tsx index b78c6f955e2b..bff23a3a365e 100644 --- a/apps/web/src/components/LegacySidebar.tsx +++ b/apps/web/src/components/LegacySidebar.tsx @@ -246,7 +246,7 @@ const PROJECT_GROUPING_MODE_LABELS: Record = separate: "Keep separate", }; const SIDEBAR_ICON_ACTION_BUTTON_CLASS = - "inline-flex h-6 min-w-6 cursor-pointer items-center justify-center rounded-md px-0.75 text-icon-muted hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring"; + "inline-flex h-6 min-w-6 cursor-pointer items-center justify-center rounded-md px-0.75 text-icon-muted hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring"; function SidebarThreadDetailPrewarmer({ threadRef }: { readonly threadRef: ScopedThreadRef }) { useEnvironmentThread(threadRef.environmentId, threadRef.threadId); @@ -769,7 +769,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP target="_blank" rel="noopener noreferrer" aria-label={prStatus.tooltip} - className={`inline-flex items-center justify-center ${prStatus.colorClass} cursor-pointer rounded-sm outline-hidden focus-visible:ring-1 focus-visible:ring-ring`} + className={`inline-flex items-center justify-center ${prStatus.colorClass} cursor-pointer rounded-sm outline-hidden focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring`} onPointerDown={(event) => event.stopPropagation()} onClick={handlePrClick} > @@ -835,7 +835,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 8d2b445a22ce..3e95f86de902 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -1817,7 +1817,7 @@ function TimelineMinimap({ /> diff --git a/apps/web/src/components/media/MediaVideoPlayer.tsx b/apps/web/src/components/media/MediaVideoPlayer.tsx index 9bf65102bf70..dd8cd0dab03f 100644 --- a/apps/web/src/components/media/MediaVideoPlayer.tsx +++ b/apps/web/src/components/media/MediaVideoPlayer.tsx @@ -203,7 +203,7 @@ export function MediaVideoPlayer({ type="button" aria-label={label ? `Play ${label}` : "Play video"} onClick={onOpen} - className="absolute inset-0 flex cursor-pointer items-center justify-center rounded-lg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring" + className="absolute inset-0 flex cursor-pointer items-center justify-center rounded-lg focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring" > diff --git a/apps/web/src/components/preview/PreviewRecentUrlCard.tsx b/apps/web/src/components/preview/PreviewRecentUrlCard.tsx index 39af63a90616..afba72c805bd 100644 --- a/apps/web/src/components/preview/PreviewRecentUrlCard.tsx +++ b/apps/web/src/components/preview/PreviewRecentUrlCard.tsx @@ -44,7 +44,7 @@ export function PreviewRecentUrlCard({ threadRef, entry, onOpen, onRemove }: Pro type="button" aria-label={`Remove ${label} from history`} onClick={onRemove} - className="absolute right-3 rounded p-1 text-muted-foreground opacity-0 hover:bg-accent hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover:opacity-100" + className="absolute right-3 rounded p-1 text-muted-foreground opacity-0 hover:bg-accent hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring group-hover:opacity-100" > diff --git a/apps/web/src/components/projectScriptEditor.tsx b/apps/web/src/components/projectScriptEditor.tsx index 19a246cf4be4..6db7e296337b 100644 --- a/apps/web/src/components/projectScriptEditor.tsx +++ b/apps/web/src/components/projectScriptEditor.tsx @@ -369,7 +369,7 @@ export function ProjectScriptEditorDialog({ type="button" className={`relative flex flex-col items-center gap-2 rounded-md border px-2 py-2 text-xs dark:border-transparent ${ isSelected - ? "border-primary/70 bg-primary/10 dark:ring-1 dark:ring-primary/30" + ? "border-primary/70 bg-primary/10 ring-inset dark:ring-1 dark:ring-primary/30" : "border-border/70 hover:bg-accent/60 dark:bg-white/[0.035]" }`} onClick={() => { diff --git a/apps/web/src/components/pullRequest/PullRequestCopyableCode.tsx b/apps/web/src/components/pullRequest/PullRequestCopyableCode.tsx index bcfb3b180113..48f71d139675 100644 --- a/apps/web/src/components/pullRequest/PullRequestCopyableCode.tsx +++ b/apps/web/src/components/pullRequest/PullRequestCopyableCode.tsx @@ -32,7 +32,7 @@ export function PullRequestCopyableCode({ diff --git a/apps/web/src/components/settings/FoldedSettingsSection.tsx b/apps/web/src/components/settings/FoldedSettingsSection.tsx index cb6f09af1001..cf4e91fcf534 100644 --- a/apps/web/src/components/settings/FoldedSettingsSection.tsx +++ b/apps/web/src/components/settings/FoldedSettingsSection.tsx @@ -46,7 +46,7 @@ export function FoldedSettingsSection({ className="flex min-h-7 items-start justify-between gap-4 px-3 sm:px-4" >

- + {title} }>
- + @@ -818,7 +818,7 @@ function KeybindingKeyControl({ onClick={() => setDraft({ isRecording: true })} aria-label={`Edit shortcut for ${commandLabel(row.command)}: ${formatShortcutLabel(row.binding.shortcut)}`} className={cn( - "inline-flex h-8 cursor-pointer items-center rounded-md border border-transparent px-1.5 sm:h-7 outline-none transition-colors hover:border-border/70 hover:bg-accent focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/24", + "inline-flex h-8 cursor-pointer items-center rounded-md border border-transparent px-1.5 sm:h-7 outline-none transition-colors hover:border-border/70 hover:bg-accent focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-inset focus-visible:ring-ring/24", pillClassName, )} > diff --git a/apps/web/src/components/settings/PanelAnimationsPreview.tsx b/apps/web/src/components/settings/PanelAnimationsPreview.tsx index 34178f9b54c7..9bc2478923b1 100644 --- a/apps/web/src/components/settings/PanelAnimationsPreview.tsx +++ b/apps/web/src/components/settings/PanelAnimationsPreview.tsx @@ -11,7 +11,7 @@ export function PanelAnimationsPreview({ durationMs }: { durationMs: number }) {

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