| author | |
| committer | |
| log | 825263b6f979e24bc7cd48a8ea0bdf18c27b58fa |
| tree | 8124baa8def24f108cb59c53003169b226b434e9 |
| parent | 25b02f4ba28d9c6a1adff3475625e5b3fdacce4b |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>86 files changed, 1137 insertions(+), 455 deletions(-)
.oxlintrc.json+4-1| ... | ... | @@ -9,6 +9,7 @@ |
| 9 | 9 | "**/routeTree.gen.ts" |
| 10 | 10 | ], |
| 11 | 11 | "plugins": ["eslint", "oxc", "react", "unicorn", "typescript"], |
| 12 | "jsPlugins": ["./oxlint-plugin-t3code/index.ts"], | |
| 12 | 13 | "categories": { |
| 13 | 14 | "correctness": "warn", |
| 14 | 15 | "suspicious": "warn", |
| ... | ... | @@ -17,6 +18,8 @@ |
| 17 | 18 | "rules": { |
| 18 | 19 | "react-in-jsx-scope": "off", |
| 19 | 20 | "eslint/no-shadow": "off", |
| 20 | "eslint/no-await-in-loop": "off" | |
| 21 | "eslint/no-await-in-loop": "off", | |
| 22 | "eslint/no-underscore-dangle": "off", | |
| 23 | "t3code/no-inline-schema-compile": "warn" | |
| 21 | 24 | } |
| 22 | 25 | } |
apps/desktop/src/app/DesktopAppIdentity.ts+2-1| ... | ... | @@ -16,6 +16,7 @@ const COMMIT_HASH_DISPLAY_LENGTH = 12; |
| 16 | 16 | const AppPackageMetadata = Schema.Struct({ |
| 17 | 17 | t3codeCommitHash: Schema.optional(Schema.String), |
| 18 | 18 | }); |
| 19 | const decodeAppPackageMetadata = Schema.decodeEffect(Schema.fromJsonString(AppPackageMetadata)); | |
| 19 | 20 | |
| 20 | 21 | export interface DesktopAppIdentityShape { |
| 21 | 22 | readonly resolveUserDataPath: Effect.Effect<string>; |
| ... | ... | @@ -47,7 +48,7 @@ const make = Effect.gen(function* () { |
| 47 | 48 | return yield* Option.match(raw, { |
| 48 | 49 | onNone: () => Effect.succeed(Option.none<string>()), |
| 49 | 50 | onSome: (value) => |
| 50 | Schema.decodeEffect(Schema.fromJsonString(AppPackageMetadata))(value).pipe( | |
| 51 | decodeAppPackageMetadata(value).pipe( | |
| 51 | 52 | Effect.map((parsed) => |
| 52 | 53 | Option.fromNullishOr(parsed.t3codeCommitHash).pipe(Option.flatMap(normalizeCommitHash)), |
| 53 | 54 | ), |
apps/desktop/src/backend/DesktopBackendManager.test.ts+5-1| ... | ... | @@ -25,6 +25,10 @@ import * as DesktopObservability from "../app/DesktopObservability.ts"; |
| 25 | 25 | import * as DesktopState from "../app/DesktopState.ts"; |
| 26 | 26 | import * as DesktopWindow from "../window/DesktopWindow.ts"; |
| 27 | 27 | |
| 28 | const decodeDesktopBackendBootstrap = Schema.decodeEffect( | |
| 29 | Schema.fromJsonString(DesktopBackendBootstrap), | |
| 30 | ); | |
| 31 | ||
| 28 | 32 | const baseConfig: DesktopBackendManager.DesktopBackendStartConfig = { |
| 29 | 33 | executablePath: "/electron", |
| 30 | 34 | entryPath: "/server/bin.mjs", |
| ... | ... | @@ -94,7 +98,7 @@ const healthyHttpClientLayer = httpClientLayer((request) => |
| 94 | 98 | ); |
| 95 | 99 | |
| 96 | 100 | function decodeBootstrap(raw: string) { |
| 97 | return Schema.decodeEffect(Schema.fromJsonString(DesktopBackendBootstrap))(raw); | |
| 101 | return decodeDesktopBackendBootstrap(raw); | |
| 98 | 102 | } |
| 99 | 103 | |
| 100 | 104 | function makeManagerLayer(input: { |
apps/desktop/src/settings/DesktopClientSettings.ts+6-2| ... | ... | @@ -20,10 +20,14 @@ const ClientSettingsDocumentSchema = Schema.Struct({ |
| 20 | 20 | |
| 21 | 21 | const ClientSettingsJson = fromLenientJson(ClientSettingsSchema); |
| 22 | 22 | const LegacyClientSettingsDocumentJson = fromLenientJson(ClientSettingsDocumentSchema); |
| 23 | const decodeLegacyClientSettingsDocumentJson = Schema.decodeEffect( | |
| 24 | LegacyClientSettingsDocumentJson, | |
| 25 | ); | |
| 26 | const decodeClientSettingsJsonValue = Schema.decodeEffect(ClientSettingsJson); | |
| 23 | 27 | const decodeClientSettingsJson = (raw: string): Effect.Effect<ClientSettings, Schema.SchemaError> => |
| 24 | Schema.decodeEffect(LegacyClientSettingsDocumentJson)(raw).pipe( | |
| 28 | decodeLegacyClientSettingsDocumentJson(raw).pipe( | |
| 25 | 29 | Effect.map((document) => document.settings), |
| 26 | Effect.catch(() => Schema.decodeEffect(ClientSettingsJson)(raw)), | |
| 30 | Effect.catch(() => decodeClientSettingsJsonValue(raw)), | |
| 27 | 31 | ); |
| 28 | 32 | const encodeClientSettingsJson = Schema.encodeEffect(ClientSettingsJson); |
| 29 | 33 |
apps/desktop/src/updates/DesktopUpdates.ts+7-4| ... | ... | @@ -54,6 +54,9 @@ const UpdateInfo = Schema.Struct({ |
| 54 | 54 | const DownloadProgressInfo = Schema.Struct({ |
| 55 | 55 | percent: Schema.Number, |
| 56 | 56 | }); |
| 57 | const decodeAppUpdateYmlConfig = Schema.decodeUnknownEffect(AppUpdateYmlConfig); | |
| 58 | const decodeUpdateInfo = Schema.decodeUnknownEffect(UpdateInfo); | |
| 59 | const decodeDownloadProgressInfo = Schema.decodeUnknownEffect(DownloadProgressInfo); | |
| 57 | 60 | |
| 58 | 61 | const currentIsoTimestamp = DateTime.now.pipe(Effect.map(DateTime.formatIso)); |
| 59 | 62 | |
| ... | ... | @@ -115,7 +118,7 @@ function parseAppUpdateYml(raw: string): Effect.Effect<Option.Option<AppUpdateYm |
| 115 | 118 | } |
| 116 | 119 | } |
| 117 | 120 | |
| 118 | return Schema.decodeUnknownEffect(AppUpdateYmlConfig)(entries).pipe( | |
| 121 | return decodeAppUpdateYmlConfig(entries).pipe( | |
| 119 | 122 | Effect.map((config) => (config.provider ? Option.some(config) : Option.none())), |
| 120 | 123 | Effect.catch(() => Effect.succeed(Option.none<AppUpdateYmlConfig>())), |
| 121 | 124 | ); |
| ... | ... | @@ -408,7 +411,7 @@ const make = Effect.gen(function* () { |
| 408 | 411 | const handleUpdateAvailable = Effect.fn("desktop.updates.handleUpdateAvailable")(function* ( |
| 409 | 412 | raw: unknown, |
| 410 | 413 | ) { |
| 411 | yield* Schema.decodeUnknownEffect(UpdateInfo)(raw).pipe( | |
| 414 | yield* decodeUpdateInfo(raw).pipe( | |
| 412 | 415 | Effect.flatMap( |
| 413 | 416 | Effect.fn("desktop.updates.applyUpdateAvailable")(function* (info) { |
| 414 | 417 | const state = yield* Ref.get(updateStateRef); |
| ... | ... | @@ -479,7 +482,7 @@ const make = Effect.gen(function* () { |
| 479 | 482 | const handleDownloadProgress = Effect.fn("desktop.updates.handleDownloadProgress")(function* ( |
| 480 | 483 | raw: unknown, |
| 481 | 484 | ) { |
| 482 | yield* Schema.decodeUnknownEffect(DownloadProgressInfo)(raw).pipe( | |
| 485 | yield* decodeDownloadProgressInfo(raw).pipe( | |
| 483 | 486 | Effect.flatMap( |
| 484 | 487 | Effect.fn("desktop.updates.applyDownloadProgress")(function* (progress) { |
| 485 | 488 | const state = yield* Ref.get(updateStateRef); |
| ... | ... | @@ -506,7 +509,7 @@ const make = Effect.gen(function* () { |
| 506 | 509 | const handleUpdateDownloaded = Effect.fn("desktop.updates.handleUpdateDownloaded")(function* ( |
| 507 | 510 | raw: unknown, |
| 508 | 511 | ) { |
| 509 | yield* Schema.decodeUnknownEffect(UpdateInfo)(raw).pipe( | |
| 512 | yield* decodeUpdateInfo(raw).pipe( | |
| 510 | 513 | Effect.flatMap( |
| 511 | 514 | Effect.fn("desktop.updates.applyUpdateDownloaded")(function* (info) { |
| 512 | 515 | const state = yield* Ref.get(updateStateRef); |
apps/server/integration/OrchestrationEngineHarness.integration.ts+3-1| ... | ... | @@ -79,6 +79,8 @@ import { VcsStatusBroadcaster } from "../src/vcs/VcsStatusBroadcaster.ts"; |
| 79 | 79 | import { GitWorkflowService } from "../src/git/GitWorkflowService.ts"; |
| 80 | 80 | import * as VcsProcess from "../src/vcs/VcsProcess.ts"; |
| 81 | 81 | |
| 82 | const decodeCodexSettings = Schema.decodeEffect(CodexSettings); | |
| 83 | ||
| 82 | 84 | function runGit(cwd: string, args: ReadonlyArray<string>) { |
| 83 | 85 | return execFileSync("git", args, { |
| 84 | 86 | cwd, |
| ... | ... | @@ -266,7 +268,7 @@ export const makeOrchestrationIntegrationHarness = ( |
| 266 | 268 | const realCodexRegistry = Layer.effect( |
| 267 | 269 | ProviderAdapterRegistry, |
| 268 | 270 | Effect.gen(function* () { |
| 269 | const codexSettings = Schema.decodeSync(CodexSettings)({}); | |
| 271 | const codexSettings = yield* decodeCodexSettings({}); | |
| 270 | 272 | const codexAdapter = yield* makeCodexAdapter(codexSettings); |
| 271 | 273 | return makeAdapterRegistryMock({ |
| 272 | 274 | [ProviderDriverKind.make("codex")]: codexAdapter, |
apps/server/src/bootstrap.test.ts+3-6| ... | ... | @@ -38,6 +38,7 @@ vi.mock("node:fs", async (importOriginal) => { |
| 38 | 38 | }); |
| 39 | 39 | |
| 40 | 40 | const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String }); |
| 41 | const encodeTestEnvelopeSchema = Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema)); | |
| 41 | 42 | |
| 42 | 43 | it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => { |
| 43 | 44 | it.effect("uses platform-specific fd paths", () => |
| ... | ... | @@ -55,9 +56,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => { |
| 55 | 56 | |
| 56 | 57 | yield* fs.writeFileString( |
| 57 | 58 | filePath, |
| 58 | `${yield* Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema))({ | |
| 59 | mode: "desktop", | |
| 60 | })}\n`, | |
| 59 | `${yield* encodeTestEnvelopeSchema({ mode: "desktop" })}\n`, | |
| 61 | 60 | ); |
| 62 | 61 | |
| 63 | 62 | const fd = yield* Effect.acquireRelease( |
| ... | ... | @@ -79,9 +78,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => { |
| 79 | 78 | |
| 80 | 79 | yield* fs.writeFileString( |
| 81 | 80 | filePath, |
| 82 | `${yield* Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema))({ | |
| 83 | mode: "desktop", | |
| 84 | })}\n`, | |
| 81 | `${yield* encodeTestEnvelopeSchema({ mode: "desktop" })}\n`, | |
| 85 | 82 | ); |
| 86 | 83 | |
| 87 | 84 | // Open without acquireRelease: the direct-stream fallback uses autoClose: true, |
apps/server/src/diagnostics/ProcessDiagnostics.ts+2-1| ... | ... | @@ -50,6 +50,7 @@ class ProcessDiagnosticsError extends Schema.TaggedErrorClass<ProcessDiagnostics |
| 50 | 50 | cause: Schema.optional(Schema.Defect), |
| 51 | 51 | }, |
| 52 | 52 | ) {} |
| 53 | const isProcessDiagnosticsError = Schema.is(ProcessDiagnosticsError); | |
| 53 | 54 | |
| 54 | 55 | function toProcessDiagnosticsError(message: string, cause?: unknown): ProcessDiagnosticsError { |
| 55 | 56 | return new ProcessDiagnosticsError({ |
| ... | ... | @@ -316,7 +317,7 @@ const runProcess = Effect.fn("runProcess")( |
| 316 | 317 | }), |
| 317 | 318 | ), |
| 318 | 319 | Effect.mapError((cause) => |
| 319 | Schema.is(ProcessDiagnosticsError)(cause) | |
| 320 | isProcessDiagnosticsError(cause) | |
| 320 | 321 | ? cause |
| 321 | 322 | : toProcessDiagnosticsError(input.errorMessage, cause), |
| 322 | 323 | ), |
apps/server/src/keybindings.test.ts+10-4| ... | ... | @@ -23,6 +23,12 @@ import { |
| 23 | 23 | import { KeybindingsConfigError } from "@t3tools/contracts"; |
| 24 | 24 | |
| 25 | 25 | const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig); |
| 26 | const encodeKeybindingsConfigJson = Schema.encodeEffect(KeybindingsConfigJson); | |
| 27 | const decodeKeybindingsConfigJson = Schema.decodeUnknownEffect(KeybindingsConfigJson); | |
| 28 | const encodeResolvedKeybindingFromConfig = Schema.encodeEffect(ResolvedKeybindingFromConfig); | |
| 29 | const decodeResolvedKeybindingFromConfigExit = Schema.decodeUnknownExit( | |
| 30 | ResolvedKeybindingFromConfig, | |
| 31 | ); | |
| 26 | 32 | const makeKeybindingsLayer = () => { |
| 27 | 33 | return KeybindingsLive.pipe( |
| 28 | 34 | Layer.provideMerge( |
| ... | ... | @@ -45,7 +51,7 @@ const writeKeybindingsConfig = (configPath: string, rules: readonly KeybindingRu |
| 45 | 51 | Effect.gen(function* () { |
| 46 | 52 | const fileSystem = yield* FileSystem.FileSystem; |
| 47 | 53 | const path = yield* Path.Path; |
| 48 | const encoded = yield* Schema.encodeEffect(KeybindingsConfigJson)(rules); | |
| 54 | const encoded = yield* encodeKeybindingsConfigJson(rules); | |
| 49 | 55 | yield* fileSystem.makeDirectory(path.dirname(configPath), { recursive: true }); |
| 50 | 56 | yield* fileSystem.writeFileString(configPath, encoded); |
| 51 | 57 | }); |
| ... | ... | @@ -54,7 +60,7 @@ const readKeybindingsConfig = (configPath: string) => |
| 54 | 60 | Effect.gen(function* () { |
| 55 | 61 | const fileSystem = yield* FileSystem.FileSystem; |
| 56 | 62 | const rawConfig = yield* fileSystem.readFileString(configPath); |
| 57 | return yield* Schema.decodeUnknownEffect(KeybindingsConfigJson)(rawConfig); | |
| 63 | return yield* decodeKeybindingsConfigJson(rawConfig); | |
| 58 | 64 | }); |
| 59 | 65 | |
| 60 | 66 | it.layer(NodeServices.layer)("keybindings", (it) => { |
| ... | ... | @@ -111,7 +117,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { |
| 111 | 117 | |
| 112 | 118 | it.effect("encodes resolved plus-key shortcuts", () => |
| 113 | 119 | Effect.gen(function* () { |
| 114 | const encoded = yield* Schema.encodeEffect(ResolvedKeybindingFromConfig)({ | |
| 120 | const encoded = yield* encodeResolvedKeybindingFromConfig({ | |
| 115 | 121 | command: "terminal.toggle", |
| 116 | 122 | shortcut: { |
| 117 | 123 | key: "+", |
| ... | ... | @@ -157,7 +163,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { |
| 157 | 163 | |
| 158 | 164 | it.effect("formats invalid resolved keybinding rules with the custom message", () => |
| 159 | 165 | Effect.sync(() => { |
| 160 | const result = Schema.decodeUnknownExit(ResolvedKeybindingFromConfig)({ | |
| 166 | const result = decodeResolvedKeybindingFromConfigExit({ | |
| 161 | 167 | key: "mod+shift+d+o", |
| 162 | 168 | command: "terminal.new", |
| 163 | 169 | }); |
apps/server/src/keybindings.ts+10-6| ... | ... | @@ -170,6 +170,10 @@ function encodeWhenAst(node: KeybindingWhenNode): string { |
| 170 | 170 | |
| 171 | 171 | const RawKeybindingsEntries = fromLenientJson(Schema.Array(Schema.Unknown)); |
| 172 | 172 | const KeybindingsConfigPrettyJson = fromJsonStringPretty(KeybindingsConfig); |
| 173 | const decodeKeybindingRuleExit = Schema.decodeUnknownExit(KeybindingRule); | |
| 174 | const decodeResolvedKeybindingFromConfigExit = Schema.decodeExit(ResolvedKeybindingFromConfig); | |
| 175 | const decodeRawKeybindingsEntriesExit = Schema.decodeUnknownExit(RawKeybindingsEntries); | |
| 176 | const encodeKeybindingsConfigPrettyJson = Schema.encodeEffect(KeybindingsConfigPrettyJson); | |
| 173 | 177 | |
| 174 | 178 | export interface KeybindingsConfigState { |
| 175 | 179 | readonly keybindings: ResolvedKeybindingsConfig; |
| ... | ... | @@ -344,7 +348,7 @@ const makeKeybindings = Effect.gen(function* () { |
| 344 | 348 | |
| 345 | 349 | return yield* Effect.forEach(rawConfig, (entry) => |
| 346 | 350 | Effect.gen(function* () { |
| 347 | const decodedRule = Schema.decodeUnknownExit(KeybindingRule)(entry); | |
| 351 | const decodedRule = decodeKeybindingRuleExit(entry); | |
| 348 | 352 | if (decodedRule._tag === "Failure") { |
| 349 | 353 | yield* Effect.logWarning("ignoring invalid keybinding entry", { |
| 350 | 354 | path: keybindingsConfigPath, |
| ... | ... | @@ -353,7 +357,7 @@ const makeKeybindings = Effect.gen(function* () { |
| 353 | 357 | }); |
| 354 | 358 | return null; |
| 355 | 359 | } |
| 356 | const resolved = Schema.decodeExit(ResolvedKeybindingFromConfig)(decodedRule.value); | |
| 360 | const resolved = decodeResolvedKeybindingFromConfigExit(decodedRule.value); | |
| 357 | 361 | if (resolved._tag === "Failure") { |
| 358 | 362 | yield* Effect.logWarning("ignoring invalid keybinding entry", { |
| 359 | 363 | path: keybindingsConfigPath, |
| ... | ... | @@ -379,7 +383,7 @@ const makeKeybindings = Effect.gen(function* () { |
| 379 | 383 | } |
| 380 | 384 | |
| 381 | 385 | const rawConfig = yield* readRawConfig; |
| 382 | const decodedEntries = Schema.decodeUnknownExit(RawKeybindingsEntries)(rawConfig); | |
| 386 | const decodedEntries = decodeRawKeybindingsEntriesExit(rawConfig); | |
| 383 | 387 | if (decodedEntries._tag === "Failure") { |
| 384 | 388 | const detail = `expected JSON array (${Cause.pretty(decodedEntries.cause)})`; |
| 385 | 389 | return { |
| ... | ... | @@ -391,7 +395,7 @@ const makeKeybindings = Effect.gen(function* () { |
| 391 | 395 | const keybindings: KeybindingRule[] = []; |
| 392 | 396 | const issues: ServerConfigIssue[] = []; |
| 393 | 397 | for (const [index, entry] of decodedEntries.value.entries()) { |
| 394 | const decodedRule = Schema.decodeUnknownExit(KeybindingRule)(entry); | |
| 398 | const decodedRule = decodeKeybindingRuleExit(entry); | |
| 395 | 399 | if (decodedRule._tag === "Failure") { |
| 396 | 400 | const detail = Cause.pretty(decodedRule.cause); |
| 397 | 401 | issues.push(invalidEntryIssue(index, detail)); |
| ... | ... | @@ -404,7 +408,7 @@ const makeKeybindings = Effect.gen(function* () { |
| 404 | 408 | continue; |
| 405 | 409 | } |
| 406 | 410 | |
| 407 | const resolvedRule = Schema.decodeExit(ResolvedKeybindingFromConfig)(decodedRule.value); | |
| 411 | const resolvedRule = decodeResolvedKeybindingFromConfigExit(decodedRule.value); | |
| 408 | 412 | if (resolvedRule._tag === "Failure") { |
| 409 | 413 | const detail = Cause.pretty(resolvedRule.cause); |
| 410 | 414 | issues.push(invalidEntryIssue(index, detail)); |
| ... | ... | @@ -423,7 +427,7 @@ const makeKeybindings = Effect.gen(function* () { |
| 423 | 427 | }); |
| 424 | 428 | |
| 425 | 429 | const writeConfigAtomically = (rules: readonly KeybindingRule[]) => { |
| 426 | return Schema.encodeEffect(KeybindingsConfigPrettyJson)(rules).pipe( | |
| 430 | return encodeKeybindingsConfigPrettyJson(rules).pipe( | |
| 427 | 431 | Effect.map((encoded) => `${encoded}\n`), |
| 428 | 432 | Effect.flatMap((encoded) => |
| 429 | 433 | writeFileStringAtomically({ |
apps/server/src/orchestration/Layers/OrchestrationEngine.ts+6-2| ... | ... | @@ -44,6 +44,10 @@ import { |
| 44 | 44 | OrchestrationEngineService, |
| 45 | 45 | type OrchestrationEngineShape, |
| 46 | 46 | } from "../Services/OrchestrationEngine.ts"; |
| 47 | const isOrchestrationCommandPreviouslyRejectedError = Schema.is( | |
| 48 | OrchestrationCommandPreviouslyRejectedError, | |
| 49 | ); | |
| 50 | const isOrchestrationCommandInvariantError = Schema.is(OrchestrationCommandInvariantError); | |
| 47 | 51 | |
| 48 | 52 | interface CommandEnvelope { |
| 49 | 53 | command: OrchestrationCommand; |
| ... | ... | @@ -245,7 +249,7 @@ const makeOrchestrationEngine = Effect.gen(function* () { |
| 245 | 249 | } |
| 246 | 250 | |
| 247 | 251 | const error = Cause.squash(exit.cause) as OrchestrationDispatchError; |
| 248 | if (!Schema.is(OrchestrationCommandPreviouslyRejectedError)(error)) { | |
| 252 | if (!isOrchestrationCommandPreviouslyRejectedError(error)) { | |
| 249 | 253 | yield* reconcileReadModelAfterDispatchFailure.pipe( |
| 250 | 254 | Effect.catch(() => |
| 251 | 255 | Effect.logWarning( |
| ... | ... | @@ -259,7 +263,7 @@ const makeOrchestrationEngine = Effect.gen(function* () { |
| 259 | 263 | ), |
| 260 | 264 | ); |
| 261 | 265 | |
| 262 | if (Schema.is(OrchestrationCommandInvariantError)(error)) { | |
| 266 | if (isOrchestrationCommandInvariantError(error)) { | |
| 263 | 267 | yield* commandReceiptRepository |
| 264 | 268 | .upsert({ |
| 265 | 269 | commandId: envelope.command.commandId, |
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+5-3| ... | ... | @@ -39,6 +39,8 @@ import { |
| 39 | 39 | import { ServerSettingsService } from "../../serverSettings.ts"; |
| 40 | 40 | import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts"; |
| 41 | 41 | import { GitWorkflowService } from "../../git/GitWorkflowService.ts"; |
| 42 | const isProviderAdapterRequestError = Schema.is(ProviderAdapterRequestError); | |
| 43 | const isProviderDriverKind = Schema.is(ProviderDriverKind); | |
| 42 | 44 | |
| 43 | 45 | type ProviderIntentEvent = Extract< |
| 44 | 46 | OrchestrationEvent, |
| ... | ... | @@ -118,7 +120,7 @@ function findProviderAdapterRequestError( |
| 118 | 120 | cause: Cause.Cause<ProviderServiceError>, |
| 119 | 121 | ): ProviderAdapterRequestError | undefined { |
| 120 | 122 | const failReason = cause.reasons.find(Cause.isFailReason); |
| 121 | return Schema.is(ProviderAdapterRequestError)(failReason?.error) ? failReason.error : undefined; | |
| 123 | return isProviderAdapterRequestError(failReason?.error) ? failReason.error : undefined; | |
| 122 | 124 | } |
| 123 | 125 | |
| 124 | 126 | function isUnknownPendingApprovalRequestError(cause: Cause.Cause<ProviderServiceError>): boolean { |
| ... | ... | @@ -233,7 +235,7 @@ const make = Effect.gen(function* () { |
| 233 | 235 | |
| 234 | 236 | const formatFailureDetail = (cause: Cause.Cause<unknown>): string => { |
| 235 | 237 | const failReason = cause.reasons.find(Cause.isFailReason); |
| 236 | const providerError = Schema.is(ProviderAdapterRequestError)(failReason?.error) | |
| 238 | const providerError = isProviderAdapterRequestError(failReason?.error) | |
| 237 | 239 | ? failReason.error |
| 238 | 240 | : undefined; |
| 239 | 241 | if (providerError) { |
| ... | ... | @@ -361,7 +363,7 @@ const make = Effect.gen(function* () { |
| 361 | 363 | ), |
| 362 | 364 | ); |
| 363 | 365 | const desiredDriverKind = desiredInfo.driverKind; |
| 364 | if (!Schema.is(ProviderDriverKind)(desiredDriverKind)) { | |
| 366 | if (!isProviderDriverKind(desiredDriverKind)) { | |
| 365 | 367 | return yield* new ProviderAdapterRequestError({ |
| 366 | 368 | provider: providerErrorLabel(String(desiredDriverKind)), |
| 367 | 369 | method: "thread.turn.start", |
apps/server/src/orchestration/projector.ts+4-5| ... | ... | @@ -47,15 +47,14 @@ function updateThread( |
| 47 | 47 | } |
| 48 | 48 | |
| 49 | 49 | function decodeForEvent<A>( |
| 50 | schema: Schema.Schema<A>, | |
| 50 | schema: Schema.Decoder<A, never>, | |
| 51 | 51 | value: unknown, |
| 52 | 52 | eventType: OrchestrationEvent["type"], |
| 53 | 53 | field: string, |
| 54 | 54 | ): Effect.Effect<A, OrchestrationProjectorDecodeError> { |
| 55 | return Effect.try({ | |
| 56 | try: () => Schema.decodeUnknownSync(schema as any)(value), | |
| 57 | catch: (error) => toProjectorDecodeError(`${eventType}:${field}`)(error as Schema.SchemaError), | |
| 58 | }); | |
| 55 | return Schema.decodeUnknownEffect(schema)(value).pipe( | |
| 56 | Effect.mapError(toProjectorDecodeError(`${eventType}:${field}`)), | |
| 57 | ); | |
| 59 | 58 | } |
| 60 | 59 | |
| 61 | 60 | function retainThreadMessagesAfterRevert( |
apps/server/src/persistence/Errors.ts+3-1| ... | ... | @@ -30,6 +30,8 @@ export class PersistenceDecodeError extends Schema.TaggedErrorClass<PersistenceD |
| 30 | 30 | return `Decode error in ${this.operation}: ${this.issue}`; |
| 31 | 31 | } |
| 32 | 32 | } |
| 33 | const isPersistenceSqlError = Schema.is(PersistenceSqlError); | |
| 34 | const isPersistenceDecodeError = Schema.is(PersistenceDecodeError); | |
| 33 | 35 | |
| 34 | 36 | export function toPersistenceSqlError(operation: string) { |
| 35 | 37 | return (cause: unknown): PersistenceSqlError => |
| ... | ... | @@ -59,7 +61,7 @@ export function toPersistenceDecodeCauseError(operation: string) { |
| 59 | 61 | } |
| 60 | 62 | |
| 61 | 63 | export const isPersistenceError = (u: unknown) => |
| 62 | Schema.is(PersistenceSqlError)(u) || Schema.is(PersistenceDecodeError)(u); | |
| 64 | isPersistenceSqlError(u) || isPersistenceDecodeError(u); | |
| 63 | 65 | |
| 64 | 66 | // =============================== |
| 65 | 67 | // Provider Session Repository Errors |
apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts+2-1| ... | ... | @@ -10,6 +10,7 @@ import { PersistenceDecodeError } from "../Errors.ts"; |
| 10 | 10 | import { OrchestrationEventStore } from "../Services/OrchestrationEventStore.ts"; |
| 11 | 11 | import { OrchestrationEventStoreLive } from "./OrchestrationEventStore.ts"; |
| 12 | 12 | import { SqlitePersistenceMemory } from "./Sqlite.ts"; |
| 13 | const isPersistenceDecodeError = Schema.is(PersistenceDecodeError); | |
| 13 | 14 | |
| 14 | 15 | const layer = it.layer( |
| 15 | 16 | OrchestrationEventStoreLive.pipe(Layer.provideMerge(SqlitePersistenceMemory)), |
| ... | ... | @@ -110,7 +111,7 @@ layer("OrchestrationEventStore", (it) => { |
| 110 | 111 | ); |
| 111 | 112 | assert.equal(replayResult._tag, "Failure"); |
| 112 | 113 | if (replayResult._tag === "Failure") { |
| 113 | assert.ok(Schema.is(PersistenceDecodeError)(replayResult.failure)); | |
| 114 | assert.ok(isPersistenceDecodeError(replayResult.failure)); | |
| 114 | 115 | assert.ok( |
| 115 | 116 | replayResult.failure.operation.includes( |
| 116 | 117 | "OrchestrationEventStore.readFromSequence:decodeRows", |
apps/server/src/provider/Drivers/ClaudeDriver.ts+2-1| ... | ... | @@ -48,6 +48,7 @@ import { |
| 48 | 48 | resolveProviderMaintenanceCapabilitiesEffect, |
| 49 | 49 | } from "../providerMaintenance.ts"; |
| 50 | 50 | import { makeClaudeCapabilitiesCacheKey, makeClaudeContinuationGroupKey } from "./ClaudeHome.ts"; |
| 51 | const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings); | |
| 51 | 52 | |
| 52 | 53 | const DRIVER_KIND = ProviderDriverKind.make("claudeAgent"); |
| 53 | 54 | const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5); |
| ... | ... | @@ -105,7 +106,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { |
| 105 | 106 | supportsMultipleInstances: true, |
| 106 | 107 | }, |
| 107 | 108 | configSchema: ClaudeSettings, |
| 108 | defaultConfig: (): ClaudeSettings => Schema.decodeSync(ClaudeSettings)({}), | |
| 109 | defaultConfig: (): ClaudeSettings => decodeClaudeSettings({}), | |
| 109 | 110 | create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => |
| 110 | 111 | Effect.gen(function* () { |
| 111 | 112 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
apps/server/src/provider/Drivers/CodexDriver.ts+2-1| ... | ... | @@ -51,6 +51,7 @@ import { |
| 51 | 51 | materializeCodexShadowHome, |
| 52 | 52 | resolveCodexHomeLayout, |
| 53 | 53 | } from "./CodexHomeLayout.ts"; |
| 54 | const decodeCodexSettings = Schema.decodeSync(CodexSettings); | |
| 54 | 55 | |
| 55 | 56 | const DRIVER_KIND = ProviderDriverKind.make("codex"); |
| 56 | 57 | const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5); |
| ... | ... | @@ -103,7 +104,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = { |
| 103 | 104 | supportsMultipleInstances: true, |
| 104 | 105 | }, |
| 105 | 106 | configSchema: CodexSettings, |
| 106 | defaultConfig: (): CodexSettings => Schema.decodeSync(CodexSettings)({}), | |
| 107 | defaultConfig: (): CodexSettings => decodeCodexSettings({}), | |
| 107 | 108 | create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => |
| 108 | 109 | Effect.gen(function* () { |
| 109 | 110 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
apps/server/src/provider/Drivers/CodexHomeLayout.test.ts+2-1| ... | ... | @@ -11,6 +11,7 @@ import { |
| 11 | 11 | materializeCodexShadowHome, |
| 12 | 12 | resolveCodexHomeLayout, |
| 13 | 13 | } from "./CodexHomeLayout.ts"; |
| 14 | const decodeCodexSettingsValue = Schema.decodeSync(CodexSettings); | |
| 14 | 15 | |
| 15 | 16 | const decodeCodexSettings = (input: { |
| 16 | 17 | readonly enabled?: boolean; |
| ... | ... | @@ -18,7 +19,7 @@ const decodeCodexSettings = (input: { |
| 18 | 19 | readonly shadowHomePath?: string; |
| 19 | 20 | readonly customModels?: readonly string[]; |
| 20 | 21 | readonly binaryPath?: string; |
| 21 | }): CodexSettings => Schema.decodeSync(CodexSettings)(input); | |
| 22 | }): CodexSettings => decodeCodexSettingsValue(input); | |
| 22 | 23 | |
| 23 | 24 | const makeTempDir = Effect.fn("CodexHomeLayout.test.makeTempDir")(function* (prefix: string) { |
| 24 | 25 | const fileSystem = yield* FileSystem.FileSystem; |
apps/server/src/provider/Drivers/CodexHomeLayout.ts+2-1| ... | ... | @@ -74,6 +74,7 @@ export class CodexShadowHomeError extends Schema.TaggedErrorClass<CodexShadowHom |
| 74 | 74 | return this.detail; |
| 75 | 75 | } |
| 76 | 76 | } |
| 77 | const isCodexShadowHomeError = Schema.is(CodexShadowHomeError); | |
| 77 | 78 | |
| 78 | 79 | type LinkState = |
| 79 | 80 | | { |
| ... | ... | @@ -88,7 +89,7 @@ type LinkState = |
| 88 | 89 | }; |
| 89 | 90 | |
| 90 | 91 | function toShadowHomeError(cause: unknown): CodexShadowHomeError { |
| 91 | return Schema.is(CodexShadowHomeError)(cause) | |
| 92 | return isCodexShadowHomeError(cause) | |
| 92 | 93 | ? cause |
| 93 | 94 | : new CodexShadowHomeError({ |
| 94 | 95 | detail: "Failed to materialize Codex shadow home.", |
apps/server/src/provider/Drivers/CursorDriver.ts+2-1| ... | ... | @@ -45,6 +45,7 @@ import { |
| 45 | 45 | makeStaticProviderMaintenanceResolver, |
| 46 | 46 | resolveProviderMaintenanceCapabilitiesEffect, |
| 47 | 47 | } from "../providerMaintenance.ts"; |
| 48 | const decodeCursorSettings = Schema.decodeSync(CursorSettings); | |
| 48 | 49 | |
| 49 | 50 | const DRIVER_KIND = ProviderDriverKind.make("cursor"); |
| 50 | 51 | const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5); |
| ... | ... | @@ -89,7 +90,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = { |
| 89 | 90 | supportsMultipleInstances: true, |
| 90 | 91 | }, |
| 91 | 92 | configSchema: CursorSettings, |
| 92 | defaultConfig: (): CursorSettings => Schema.decodeSync(CursorSettings)({}), | |
| 93 | defaultConfig: (): CursorSettings => decodeCursorSettings({}), | |
| 93 | 94 | create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => |
| 94 | 95 | Effect.gen(function* () { |
| 95 | 96 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
apps/server/src/provider/Drivers/OpenCodeDriver.ts+2-1| ... | ... | @@ -46,6 +46,7 @@ import { |
| 46 | 46 | normalizeCommandPath, |
| 47 | 47 | resolveProviderMaintenanceCapabilitiesEffect, |
| 48 | 48 | } from "../providerMaintenance.ts"; |
| 49 | const decodeOpenCodeSettings = Schema.decodeSync(OpenCodeSettings); | |
| 49 | 50 | |
| 50 | 51 | const DRIVER_KIND = ProviderDriverKind.make("opencode"); |
| 51 | 52 | const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5); |
| ... | ... | @@ -102,7 +103,7 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv> |
| 102 | 103 | supportsMultipleInstances: true, |
| 103 | 104 | }, |
| 104 | 105 | configSchema: OpenCodeSettings, |
| 105 | defaultConfig: (): OpenCodeSettings => Schema.decodeSync(OpenCodeSettings)({}), | |
| 106 | defaultConfig: (): OpenCodeSettings => decodeOpenCodeSettings({}), | |
| 106 | 107 | create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => |
| 107 | 108 | Effect.gen(function* () { |
| 108 | 109 | const openCodeRuntime = yield* OpenCodeRuntime; |
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+4-3| ... | ... | @@ -38,6 +38,7 @@ import { ServerSettingsService } from "../../serverSettings.ts"; |
| 38 | 38 | import { ProviderAdapterValidationError } from "../Errors.ts"; |
| 39 | 39 | import type { ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts"; |
| 40 | 40 | import { makeClaudeAdapter, type ClaudeAdapterLiveOptions } from "./ClaudeAdapter.ts"; |
| 41 | const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings); | |
| 41 | 42 | |
| 42 | 43 | // Test-local service tag so the rest of the file can keep using `yield* ClaudeAdapter`. |
| 43 | 44 | class ClaudeAdapter extends Context.Service<ClaudeAdapter, ClaudeAdapterShape>()( |
| ... | ... | @@ -186,7 +187,7 @@ function makeHarness(config?: { |
| 186 | 187 | layer: Layer.effect( |
| 187 | 188 | ClaudeAdapter, |
| 188 | 189 | Effect.gen(function* () { |
| 189 | const claudeConfig = Schema.decodeSync(ClaudeSettings)(config?.claudeConfig ?? {}); | |
| 190 | const claudeConfig = decodeClaudeSettings(config?.claudeConfig ?? {}); | |
| 190 | 191 | return yield* makeClaudeAdapter(claudeConfig, adapterOptions); |
| 191 | 192 | }), |
| 192 | 193 | ).pipe( |
| ... | ... | @@ -1344,7 +1345,7 @@ describe("ClaudeAdapterLive", () => { |
| 1344 | 1345 | const layer = Layer.effect( |
| 1345 | 1346 | ClaudeAdapter, |
| 1346 | 1347 | Effect.gen(function* () { |
| 1347 | const claudeConfig = Schema.decodeSync(ClaudeSettings)({}); | |
| 1348 | const claudeConfig = decodeClaudeSettings({}); | |
| 1348 | 1349 | return yield* makeClaudeAdapter(claudeConfig, { |
| 1349 | 1350 | createQuery: () => { |
| 1350 | 1351 | const query = new FakeClaudeQuery(); |
| ... | ... | @@ -1427,7 +1428,7 @@ describe("ClaudeAdapterLive", () => { |
| 1427 | 1428 | const layer = Layer.effect( |
| 1428 | 1429 | ClaudeAdapter, |
| 1429 | 1430 | Effect.gen(function* () { |
| 1430 | const claudeConfig = Schema.decodeSync(ClaudeSettings)({}); | |
| 1431 | const claudeConfig = decodeClaudeSettings({}); | |
| 1431 | 1432 | return yield* makeClaudeAdapter(claudeConfig, { |
| 1432 | 1433 | createQuery: (input) => { |
| 1433 | 1434 | // Simulate the SDK consuming the prompt iterable |
apps/server/src/provider/Layers/ClaudeAdapter.ts+17-18| ... | ... | @@ -84,6 +84,8 @@ import { |
| 84 | 84 | } from "../Errors.ts"; |
| 85 | 85 | import { type ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts"; |
| 86 | 86 | import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; |
| 87 | const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJsonString); | |
| 88 | const decodeUnknownJsonStringExit = Schema.decodeUnknownExit(Schema.UnknownFromJsonString); | |
| 87 | 89 | |
| 88 | 90 | const PROVIDER = ProviderDriverKind.make("claudeAgent"); |
| 89 | 91 | type ClaudeTextStreamKind = Extract<RuntimeContentStreamKind, "assistant_text" | "reasoning_text">; |
| ... | ... | @@ -93,6 +95,11 @@ type ClaudeToolResultStreamKind = Extract< |
| 93 | 95 | >; |
| 94 | 96 | type ClaudeSdkEffort = NonNullable<ClaudeQueryOptions["effort"]>; |
| 95 | 97 | |
| 98 | function encodeJsonStringForDiagnostics(input: unknown): string | undefined { | |
| 99 | const result = encodeUnknownJsonStringExit(input); | |
| 100 | return Exit.isSuccess(result) ? result.value : undefined; | |
| 101 | } | |
| 102 | ||
| 96 | 103 | type PromptQueueItem = |
| 97 | 104 | | { |
| 98 | 105 | readonly type: "message"; |
| ... | ... | @@ -545,7 +552,7 @@ function summarizeToolRequest(toolName: string, input: Record<string, unknown>): |
| 545 | 552 | } |
| 546 | 553 | } |
| 547 | 554 | |
| 548 | const serialized = Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(input); | |
| 555 | const serialized = encodeJsonStringForDiagnostics(input) ?? "[unserializable input]"; | |
| 549 | 556 | if (serialized.length <= 400) { |
| 550 | 557 | return `${toolName}: ${serialized}`; |
| 551 | 558 | } |
| ... | ... | @@ -810,22 +817,18 @@ function exitPlanCaptureKey(input: { |
| 810 | 817 | } |
| 811 | 818 | |
| 812 | 819 | function tryParseJsonRecord(value: string): Record<string, unknown> | undefined { |
| 813 | try { | |
| 814 | const parsed = Schema.decodeUnknownSync(Schema.UnknownFromJsonString)(value); | |
| 815 | return parsed && typeof parsed === "object" && !Array.isArray(parsed) | |
| 816 | ? (parsed as Record<string, unknown>) | |
| 817 | : undefined; | |
| 818 | } catch { | |
| 820 | const result = decodeUnknownJsonStringExit(value); | |
| 821 | if (!Exit.isSuccess(result)) { | |
| 819 | 822 | return undefined; |
| 820 | 823 | } |
| 824 | const parsed = result.value; | |
| 825 | return parsed && typeof parsed === "object" && !Array.isArray(parsed) | |
| 826 | ? (parsed as Record<string, unknown>) | |
| 827 | : undefined; | |
| 821 | 828 | } |
| 822 | 829 | |
| 823 | 830 | function toolInputFingerprint(input: Record<string, unknown>): string | undefined { |
| 824 | try { | |
| 825 | return Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(input); | |
| 826 | } catch { | |
| 827 | return undefined; | |
| 828 | } | |
| 831 | return encodeJsonStringForDiagnostics(input); | |
| 829 | 832 | } |
| 830 | 833 | |
| 831 | 834 | function toolResultStreamKind(itemType: CanonicalItemType): ClaudeToolResultStreamKind | undefined { |
| ... | ... | @@ -2941,12 +2944,8 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 2941 | 2944 | "claude.query.include_partial_messages": true, |
| 2942 | 2945 | "claude.query.additional_directories": input.cwd ? [input.cwd] : [], |
| 2943 | 2946 | "claude.query.setting_sources": [...CLAUDE_SETTING_SOURCES], |
| 2944 | "claude.query.settings_json": Schema.encodeUnknownSync(Schema.UnknownFromJsonString)( | |
| 2945 | settings, | |
| 2946 | ), | |
| 2947 | "claude.query.extra_args_json": Schema.encodeUnknownSync(Schema.UnknownFromJsonString)( | |
| 2948 | extraArgs, | |
| 2949 | ), | |
| 2947 | "claude.query.settings_json": encodeJsonStringForDiagnostics(settings) ?? "", | |
| 2948 | "claude.query.extra_args_json": encodeJsonStringForDiagnostics(extraArgs) ?? "", | |
| 2950 | 2949 | "claude.query.path_to_executable": claudeBinaryPath, |
| 2951 | 2950 | }); |
| 2952 | 2951 |
apps/server/src/provider/Layers/CodexAdapter.test.ts+8-7| ... | ... | @@ -46,6 +46,7 @@ import { |
| 46 | 46 | type CodexThreadSnapshot, |
| 47 | 47 | } from "./CodexSessionRuntime.ts"; |
| 48 | 48 | import { makeCodexAdapter } from "./CodexAdapter.ts"; |
| 49 | const decodeCodexSettings = Schema.decodeSync(CodexSettings); | |
| 49 | 50 | |
| 50 | 51 | // Test-local service tag so the rest of the file can keep using `yield* CodexAdapter`. |
| 51 | 52 | class CodexAdapter extends Context.Service<CodexAdapter, CodexAdapterShape>()( |
| ... | ... | @@ -224,7 +225,7 @@ const validationLayer = it.layer( |
| 224 | 225 | Layer.effect( |
| 225 | 226 | CodexAdapter, |
| 226 | 227 | Effect.gen(function* () { |
| 227 | const codexConfig = Schema.decodeSync(CodexSettings)({}); | |
| 228 | const codexConfig = decodeCodexSettings({}); | |
| 228 | 229 | return yield* makeCodexAdapter(codexConfig, { |
| 229 | 230 | makeRuntime: validationRuntimeFactory.factory, |
| 230 | 231 | }); |
| ... | ... | @@ -293,7 +294,7 @@ const sessionErrorLayer = it.layer( |
| 293 | 294 | Layer.effect( |
| 294 | 295 | CodexAdapter, |
| 295 | 296 | Effect.gen(function* () { |
| 296 | const codexConfig = Schema.decodeSync(CodexSettings)({}); | |
| 297 | const codexConfig = decodeCodexSettings({}); | |
| 297 | 298 | return yield* makeCodexAdapter(codexConfig, { |
| 298 | 299 | makeRuntime: sessionRuntimeFactory.factory, |
| 299 | 300 | }); |
| ... | ... | @@ -364,7 +365,7 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => { |
| 364 | 365 | const customLayer = Layer.effect( |
| 365 | 366 | CodexAdapter, |
| 366 | 367 | Effect.gen(function* () { |
| 367 | const codexConfig = Schema.decodeSync(CodexSettings)({}); | |
| 368 | const codexConfig = decodeCodexSettings({}); | |
| 368 | 369 | return yield* makeCodexAdapter(codexConfig, { |
| 369 | 370 | instanceId: customInstanceId, |
| 370 | 371 | makeRuntime: customRuntimeFactory.factory, |
| ... | ... | @@ -419,7 +420,7 @@ const lifecycleLayer = it.layer( |
| 419 | 420 | Layer.effect( |
| 420 | 421 | CodexAdapter, |
| 421 | 422 | Effect.gen(function* () { |
| 422 | const codexConfig = Schema.decodeSync(CodexSettings)({}); | |
| 423 | const codexConfig = decodeCodexSettings({}); | |
| 423 | 424 | return yield* makeCodexAdapter(codexConfig, { |
| 424 | 425 | makeRuntime: lifecycleRuntimeFactory.factory, |
| 425 | 426 | }); |
| ... | ... | @@ -1034,7 +1035,7 @@ const scopedLifecycleLayer = it.layer( |
| 1034 | 1035 | Layer.effect( |
| 1035 | 1036 | CodexAdapter, |
| 1036 | 1037 | Effect.gen(function* () { |
| 1037 | const codexConfig = Schema.decodeSync(CodexSettings)({}); | |
| 1038 | const codexConfig = decodeCodexSettings({}); | |
| 1038 | 1039 | return yield* makeCodexAdapter(codexConfig, { |
| 1039 | 1040 | makeRuntime: scopedLifecycleRuntimeFactory.factory, |
| 1040 | 1041 | }); |
| ... | ... | @@ -1078,7 +1079,7 @@ const scopedFailureLayer = it.layer( |
| 1078 | 1079 | Layer.effect( |
| 1079 | 1080 | CodexAdapter, |
| 1080 | 1081 | Effect.gen(function* () { |
| 1081 | const codexConfig = Schema.decodeSync(CodexSettings)({}); | |
| 1082 | const codexConfig = decodeCodexSettings({}); | |
| 1082 | 1083 | return yield* makeCodexAdapter(codexConfig, { |
| 1083 | 1084 | makeRuntime: scopedFailureRuntimeFactory.factory, |
| 1084 | 1085 | }); |
| ... | ... | @@ -1127,7 +1128,7 @@ it.effect("flushes managed native logs when the adapter layer shuts down", () => |
| 1127 | 1128 | const layer = Layer.effect( |
| 1128 | 1129 | CodexAdapter, |
| 1129 | 1130 | Effect.gen(function* () { |
| 1130 | const codexConfig = Schema.decodeSync(CodexSettings)({}); | |
| 1131 | const codexConfig = decodeCodexSettings({}); | |
| 1131 | 1132 | return yield* makeCodexAdapter(codexConfig, { |
| 1132 | 1133 | makeRuntime: runtimeFactory.factory, |
| 1133 | 1134 | nativeEventLogPath: basePath, |
apps/server/src/provider/Layers/CodexAdapter.ts+11-7| ... | ... | @@ -61,6 +61,12 @@ import { |
| 61 | 61 | type CodexSessionRuntimeShape, |
| 62 | 62 | } from "./CodexSessionRuntime.ts"; |
| 63 | 63 | import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; |
| 64 | const isCodexAppServerProcessExitedError = Schema.is(CodexErrors.CodexAppServerProcessExitedError); | |
| 65 | const isCodexAppServerTransportError = Schema.is(CodexErrors.CodexAppServerTransportError); | |
| 66 | const isCodexSessionRuntimeThreadIdMissingError = Schema.is( | |
| 67 | CodexSessionRuntimeThreadIdMissingError, | |
| 68 | ); | |
| 69 | const isCodexResumeCursorSchema = Schema.is(CodexResumeCursorSchema); | |
| 64 | 70 | |
| 65 | 71 | const PROVIDER = ProviderDriverKind.make("codex"); |
| 66 | 72 | |
| ... | ... | @@ -91,10 +97,7 @@ function mapCodexRuntimeError( |
| 91 | 97 | method: string, |
| 92 | 98 | error: CodexSessionRuntimeError, |
| 93 | 99 | ): ProviderAdapterError { |
| 94 | if ( | |
| 95 | Schema.is(CodexErrors.CodexAppServerProcessExitedError)(error) || | |
| 96 | Schema.is(CodexErrors.CodexAppServerTransportError)(error) | |
| 97 | ) { | |
| 100 | if (isCodexAppServerProcessExitedError(error) || isCodexAppServerTransportError(error)) { | |
| 98 | 101 | return new ProviderAdapterSessionClosedError({ |
| 99 | 102 | provider: PROVIDER, |
| 100 | 103 | threadId, |
| ... | ... | @@ -102,7 +105,7 @@ function mapCodexRuntimeError( |
| 102 | 105 | }); |
| 103 | 106 | } |
| 104 | 107 | |
| 105 | if (Schema.is(CodexSessionRuntimeThreadIdMissingError)(error)) { | |
| 108 | if (isCodexSessionRuntimeThreadIdMissingError(error)) { | |
| 106 | 109 | return new ProviderAdapterSessionNotFoundError({ |
| 107 | 110 | provider: PROVIDER, |
| 108 | 111 | threadId, |
| ... | ... | @@ -134,7 +137,8 @@ function readPayload<A>( |
| 134 | 137 | schema: Schema.Schema<A>, |
| 135 | 138 | payload: ProviderEvent["payload"], |
| 136 | 139 | ): A | undefined { |
| 137 | return Schema.is(schema)(payload) ? payload : undefined; | |
| 140 | const isPayload = Schema.is(schema); | |
| 141 | return isPayload(payload) ? payload : undefined; | |
| 138 | 142 | } |
| 139 | 143 | |
| 140 | 144 | function trimText(value: string | undefined | null): string | undefined { |
| ... | ... | @@ -1381,7 +1385,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( |
| 1381 | 1385 | binaryPath: codexConfig.binaryPath, |
| 1382 | 1386 | ...(options?.environment ? { environment: options.environment } : {}), |
| 1383 | 1387 | ...(codexConfig.homePath ? { homePath: codexConfig.homePath } : {}), |
| 1384 | ...(Schema.is(CodexResumeCursorSchema)(input.resumeCursor) | |
| 1388 | ...(isCodexResumeCursorSchema(input.resumeCursor) | |
| 1385 | 1389 | ? { resumeCursor: input.resumeCursor } |
| 1386 | 1390 | : {}), |
| 1387 | 1391 | runtimeMode: input.runtimeMode, |
apps/server/src/provider/Layers/CodexProvider.ts+2-1| ... | ... | @@ -26,6 +26,7 @@ import { buildServerProvider, type ServerProviderDraft } from "../providerSnapsh |
| 26 | 26 | import { expandHomePath } from "../../pathExpansion.ts"; |
| 27 | 27 | import { scopedSafeTeardown } from "./scopedSafeTeardown.ts"; |
| 28 | 28 | import packageJson from "../../../package.json" with { type: "json" }; |
| 29 | const isCodexAppServerSpawnError = Schema.is(CodexErrors.CodexAppServerSpawnError); | |
| 29 | 30 | |
| 30 | 31 | const PROVIDER_PROBE_TIMEOUT_MS = 8_000; |
| 31 | 32 | const CODEX_PRESENTATION = { |
| ... | ... | @@ -450,7 +451,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu |
| 450 | 451 | |
| 451 | 452 | if (Result.isFailure(probeResult)) { |
| 452 | 453 | const error = probeResult.failure; |
| 453 | const installed = !Schema.is(CodexErrors.CodexAppServerSpawnError)(error); | |
| 454 | const installed = !isCodexAppServerSpawnError(error); | |
| 454 | 455 | return buildServerProvider({ |
| 455 | 456 | presentation: CODEX_PRESENTATION, |
| 456 | 457 | enabled: codexSettings.enabled, |
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts+2-1| ... | ... | @@ -16,6 +16,7 @@ import { |
| 16 | 16 | isRecoverableThreadResumeError, |
| 17 | 17 | openCodexThread, |
| 18 | 18 | } from "./CodexSessionRuntime.ts"; |
| 19 | const isCodexAppServerRequestError = Schema.is(CodexErrors.CodexAppServerRequestError); | |
| 19 | 20 | |
| 20 | 21 | function makeThreadOpenResponse( |
| 21 | 22 | threadId: string, |
| ... | ... | @@ -269,7 +270,7 @@ describe("openCodexThread", () => { |
| 269 | 270 | }), |
| 270 | 271 | ), |
| 271 | 272 | (error: unknown) => |
| 272 | Schema.is(CodexErrors.CodexAppServerRequestError)(error) && | |
| 273 | isCodexAppServerRequestError(error) && | |
| 273 | 274 | error.errorMessage === "timed out waiting for server", |
| 274 | 275 | ); |
| 275 | 276 | }); |
apps/server/src/provider/Layers/CodexSessionRuntime.ts+10-6| ... | ... | @@ -41,6 +41,7 @@ import { |
| 41 | 41 | CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS, |
| 42 | 42 | CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS, |
| 43 | 43 | } from "../CodexDeveloperInstructions.ts"; |
| 44 | const decodeV2TurnStartResponse = Schema.decodeUnknownEffect(EffectCodexSchema.V2TurnStartResponse); | |
| 44 | 45 | |
| 45 | 46 | const PROVIDER = ProviderDriverKind.make("codex"); |
| 46 | 47 | |
| ... | ... | @@ -66,6 +67,8 @@ export const CodexResumeCursorSchema = Schema.Struct({ |
| 66 | 67 | const CodexUserInputAnswerObject = Schema.Struct({ |
| 67 | 68 | answers: Schema.Array(Schema.String), |
| 68 | 69 | }); |
| 70 | const isCodexResumeCursorSchema = Schema.is(CodexResumeCursorSchema); | |
| 71 | const isCodexUserInputAnswerObject = Schema.is(CodexUserInputAnswerObject); | |
| 69 | 72 | |
| 70 | 73 | // TODO: Verify `packages/effect-codex-app-server/scripts/generate.ts` so the generated |
| 71 | 74 | // `V2TurnStartParams` schema includes `collaborationMode` directly. |
| ... | ... | @@ -74,6 +77,9 @@ const CodexTurnStartParamsWithCollaborationMode = EffectCodexSchema.V2TurnStartP |
| 74 | 77 | collaborationMode: Schema.optionalKey(EffectCodexSchema.V2TurnStartParams__CollaborationMode), |
| 75 | 78 | }), |
| 76 | 79 | ); |
| 80 | const decodeCodexTurnStartParamsWithCollaborationMode = Schema.decodeUnknownEffect( | |
| 81 | CodexTurnStartParamsWithCollaborationMode, | |
| 82 | ); | |
| 77 | 83 | |
| 78 | 84 | export type CodexTurnStartParamsWithCollaborationMode = |
| 79 | 85 | typeof CodexTurnStartParamsWithCollaborationMode.Type; |
| ... | ... | @@ -248,7 +254,7 @@ function normalizeCodexModelSlug( |
| 248 | 254 | function readResumeCursorThreadId( |
| 249 | 255 | resumeCursor: ProviderSession["resumeCursor"], |
| 250 | 256 | ): string | undefined { |
| 251 | return Schema.is(CodexResumeCursorSchema)(resumeCursor) ? resumeCursor.threadId : undefined; | |
| 257 | return isCodexResumeCursorSchema(resumeCursor) ? resumeCursor.threadId : undefined; | |
| 252 | 258 | } |
| 253 | 259 | |
| 254 | 260 | function runtimeModeToThreadConfig(input: RuntimeMode): { |
| ... | ... | @@ -367,7 +373,7 @@ export function buildTurnStartParams(input: { |
| 367 | 373 | ...(input.effort ? { effort: input.effort } : {}), |
| 368 | 374 | }); |
| 369 | 375 | |
| 370 | return Schema.decodeUnknownEffect(CodexTurnStartParamsWithCollaborationMode)({ | |
| 376 | return decodeCodexTurnStartParamsWithCollaborationMode({ | |
| 371 | 377 | threadId: input.threadId, |
| 372 | 378 | input: turnInput, |
| 373 | 379 | approvalPolicy: config.approvalPolicy, |
| ... | ... | @@ -623,7 +629,7 @@ function toCodexUserInputAnswer( |
| 623 | 629 | const answers = value.filter((entry): entry is string => typeof entry === "string"); |
| 624 | 630 | return Effect.succeed({ answers }); |
| 625 | 631 | } |
| 626 | if (Schema.is(CodexUserInputAnswerObject)(value)) { | |
| 632 | if (isCodexUserInputAnswerObject(value)) { | |
| 627 | 633 | return Effect.succeed({ answers: value.answers }); |
| 628 | 634 | } |
| 629 | 635 | return Effect.fail(new CodexSessionRuntimeInvalidUserInputAnswersError({ questionId })); |
| ... | ... | @@ -1247,9 +1253,7 @@ export const makeCodexSessionRuntime = ( |
| 1247 | 1253 | ...(input.interactionMode ? { interactionMode: input.interactionMode } : {}), |
| 1248 | 1254 | }); |
| 1249 | 1255 | const rawResponse = yield* client.raw.request("turn/start", params); |
| 1250 | const response = yield* Schema.decodeUnknownEffect(EffectCodexSchema.V2TurnStartResponse)( | |
| 1251 | rawResponse, | |
| 1252 | ).pipe( | |
| 1256 | const response = yield* decodeV2TurnStartResponse(rawResponse).pipe( | |
| 1253 | 1257 | Effect.mapError((error) => |
| 1254 | 1258 | toProtocolParseError("Invalid turn/start response payload", error), |
| 1255 | 1259 | ), |
apps/server/src/provider/Layers/CursorAdapter.test.ts+4-3| ... | ... | @@ -28,6 +28,7 @@ import { ServerConfig } from "../../config.ts"; |
| 28 | 28 | import { ServerSettingsService } from "../../serverSettings.ts"; |
| 29 | 29 | import type { CursorAdapterShape } from "../Services/CursorAdapter.ts"; |
| 30 | 30 | import { makeCursorAdapter } from "./CursorAdapter.ts"; |
| 31 | const decodeCursorSettings = Schema.decodeSync(CursorSettings); | |
| 31 | 32 | |
| 32 | 33 | // Test-local service tag so the rest of the file can keep using `yield* CursorAdapter`. |
| 33 | 34 | class CursorAdapter extends Context.Service<CursorAdapter, CursorAdapterShape>()( |
| ... | ... | @@ -132,7 +133,7 @@ const cursorAdapterTestLayer = it.layer( |
| 132 | 133 | Layer.effect( |
| 133 | 134 | CursorAdapter, |
| 134 | 135 | Effect.gen(function* () { |
| 135 | const cursorConfig = Schema.decodeSync(CursorSettings)({}); | |
| 136 | const cursorConfig = decodeCursorSettings({}); | |
| 136 | 137 | const resolveSettings = yield* makeResolveCursorSettings; |
| 137 | 138 | return yield* makeCursorAdapter(cursorConfig, { resolveSettings }); |
| 138 | 139 | }), |
| ... | ... | @@ -607,7 +608,7 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => { |
| 607 | 608 | Layer.effect( |
| 608 | 609 | CursorAdapter, |
| 609 | 610 | Effect.gen(function* () { |
| 610 | const cursorConfig = Schema.decodeSync(CursorSettings)({}); | |
| 611 | const cursorConfig = decodeCursorSettings({}); | |
| 611 | 612 | const resolveSettings = yield* makeResolveCursorSettings; |
| 612 | 613 | return yield* makeCursorAdapter(cursorConfig, { resolveSettings }); |
| 613 | 614 | }), |
| ... | ... | @@ -1239,7 +1240,7 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => { |
| 1239 | 1240 | const customAdapterLayer = Layer.effect( |
| 1240 | 1241 | CursorAdapter, |
| 1241 | 1242 | Effect.gen(function* () { |
| 1242 | const cursorConfig = Schema.decodeSync(CursorSettings)({}); | |
| 1243 | const cursorConfig = decodeCursorSettings({}); | |
| 1243 | 1244 | const resolveSettings = yield* makeResolveCursorSettings; |
| 1244 | 1245 | return yield* makeCursorAdapter(cursorConfig, { |
| 1245 | 1246 | instanceId: customInstanceId, |
apps/server/src/provider/Layers/CursorAdapter.ts+9-2| ... | ... | @@ -75,6 +75,7 @@ import { |
| 75 | 75 | import { type CursorAdapterShape } from "../Services/CursorAdapter.ts"; |
| 76 | 76 | import { resolveCursorAcpBaseModelId } from "./CursorProvider.ts"; |
| 77 | 77 | import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; |
| 78 | const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJsonString); | |
| 78 | 79 | |
| 79 | 80 | const PROVIDER = ProviderDriverKind.make("cursor"); |
| 80 | 81 | const CURSOR_RESUME_VERSION = 1 as const; |
| ... | ... | @@ -82,6 +83,11 @@ const ACP_PLAN_MODE_ALIASES = ["plan", "architect"]; |
| 82 | 83 | const ACP_IMPLEMENT_MODE_ALIASES = ["code", "agent", "default", "chat", "implement"]; |
| 83 | 84 | const ACP_APPROVAL_MODE_ALIASES = ["ask"]; |
| 84 | 85 | |
| 86 | function encodeJsonStringForDiagnostics(input: unknown): string | undefined { | |
| 87 | const result = encodeUnknownJsonStringExit(input); | |
| 88 | return Exit.isSuccess(result) ? result.value : undefined; | |
| 89 | } | |
| 90 | ||
| 85 | 91 | export interface CursorAdapterLiveOptions { |
| 86 | 92 | readonly environment?: NodeJS.ProcessEnv; |
| 87 | 93 | readonly nativeEventLogPath?: string; |
| ... | ... | @@ -390,7 +396,7 @@ export function makeCursorAdapter( |
| 390 | 396 | method: string, |
| 391 | 397 | ) => |
| 392 | 398 | Effect.gen(function* () { |
| 393 | const fingerprint = `${ctx.activeTurnId ?? "no-turn"}:${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(payload)}`; | |
| 399 | const fingerprint = `${ctx.activeTurnId ?? "no-turn"}:${encodeJsonStringForDiagnostics(payload) ?? "[unserializable payload]"}`; | |
| 394 | 400 | if (ctx.lastPlanFingerprint === fingerprint) { |
| 395 | 401 | return; |
| 396 | 402 | } |
| ... | ... | @@ -640,7 +646,8 @@ export function makeCursorAdapter( |
| 640 | 646 | permissionRequest, |
| 641 | 647 | detail: |
| 642 | 648 | permissionRequest.detail ?? |
| 643 | Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(params).slice(0, 2000), | |
| 649 | encodeJsonStringForDiagnostics(params)?.slice(0, 2000) ?? | |
| 650 | "[unserializable params]", | |
| 644 | 651 | args: params, |
| 645 | 652 | source: "acp.jsonrpc", |
| 646 | 653 | method: "session/request_permission", |
apps/server/src/provider/Layers/OpenCodeProvider.test.ts+2-1| ... | ... | @@ -16,6 +16,7 @@ import { |
| 16 | 16 | } from "../opencodeRuntime.ts"; |
| 17 | 17 | import { checkOpenCodeProviderStatus } from "./OpenCodeProvider.ts"; |
| 18 | 18 | import type { OpenCodeInventory } from "../opencodeRuntime.ts"; |
| 19 | const decodeOpenCodeSettings = Schema.decodeSync(OpenCodeSettings); | |
| 19 | 20 | |
| 20 | 21 | const DEFAULT_VERSION_STDOUT = "opencode 1.14.19\n"; |
| 21 | 22 | |
| ... | ... | @@ -106,7 +107,7 @@ const testLayer = Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble).pipe |
| 106 | 107 | ); |
| 107 | 108 | |
| 108 | 109 | const makeOpenCodeSettings = (overrides?: Partial<OpenCodeSettings>): OpenCodeSettings => |
| 109 | Schema.decodeSync(OpenCodeSettings)({ | |
| 110 | decodeOpenCodeSettings({ | |
| 110 | 111 | enabled: true, |
| 111 | 112 | binaryPath: "opencode", |
| 112 | 113 | serverUrl: "", |
apps/server/src/provider/Layers/ProviderRegistry.test.ts+6-5| ... | ... | @@ -47,6 +47,7 @@ import type { ProviderInstance } from "../ProviderDriver.ts"; |
| 47 | 47 | import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts"; |
| 48 | 48 | import { ProviderRegistry } from "../Services/ProviderRegistry.ts"; |
| 49 | 49 | import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts"; |
| 50 | const decodeServerSettings = Schema.decodeSync(ServerSettings); | |
| 50 | 51 | |
| 51 | 52 | const defaultClaudeSettings: ClaudeSettings = Schema.decodeSync(ClaudeSettings)({}); |
| 52 | 53 | const defaultCodexSettings: CodexSettings = Schema.decodeSync(CodexSettings)({}); |
| ... | ... | @@ -255,7 +256,7 @@ function makeMutableServerSettingsService( |
| 255 | 256 | updateSettings: (patch) => |
| 256 | 257 | Effect.gen(function* () { |
| 257 | 258 | const current = yield* Ref.get(settingsRef); |
| 258 | const next = Schema.decodeSync(ServerSettings)(deepMerge(current, patch)); | |
| 259 | const next = decodeServerSettings(deepMerge(current, patch)); | |
| 259 | 260 | yield* Ref.set(settingsRef, next); |
| 260 | 261 | yield* PubSub.publish(changes, next); |
| 261 | 262 | return next; |
| ... | ... | @@ -928,7 +929,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T |
| 928 | 929 | Effect.gen(function* () { |
| 929 | 930 | const missingBinary = `t3code_codex_missing_`; |
| 930 | 931 | const serverSettings = yield* makeMutableServerSettingsService( |
| 931 | Schema.decodeSync(ServerSettings)( | |
| 932 | decodeServerSettings( | |
| 932 | 933 | deepMerge(DEFAULT_SERVER_SETTINGS, { |
| 933 | 934 | providers: { |
| 934 | 935 | // Disable every built-in probe that would otherwise spawn |
| ... | ... | @@ -1027,7 +1028,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T |
| 1027 | 1028 | const firstMissing = `t3code_codex_first_`; |
| 1028 | 1029 | const secondMissing = `t3code_codex_second_`; |
| 1029 | 1030 | const serverSettings = yield* makeMutableServerSettingsService( |
| 1030 | Schema.decodeSync(ServerSettings)( | |
| 1031 | decodeServerSettings( | |
| 1031 | 1032 | deepMerge(DEFAULT_SERVER_SETTINGS, { |
| 1032 | 1033 | providers: { |
| 1033 | 1034 | codex: { enabled: true, binaryPath: firstMissing }, |
| ... | ... | @@ -1122,7 +1123,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T |
| 1122 | 1123 | it.effect("includes unavailable instance snapshots in getProviders", () => |
| 1123 | 1124 | Effect.gen(function* () { |
| 1124 | 1125 | const serverSettings = yield* makeMutableServerSettingsService( |
| 1125 | Schema.decodeSync(ServerSettings)( | |
| 1126 | decodeServerSettings( | |
| 1126 | 1127 | deepMerge(DEFAULT_SERVER_SETTINGS, { |
| 1127 | 1128 | providers: { |
| 1128 | 1129 | codex: { enabled: false }, |
| ... | ... | @@ -1178,7 +1179,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T |
| 1178 | 1179 | () => |
| 1179 | 1180 | Effect.gen(function* () { |
| 1180 | 1181 | const serverSettings = yield* makeMutableServerSettingsService( |
| 1181 | Schema.decodeSync(ServerSettings)( | |
| 1182 | decodeServerSettings( | |
| 1182 | 1183 | deepMerge(DEFAULT_SERVER_SETTINGS, { |
| 1183 | 1184 | providers: { |
| 1184 | 1185 | codex: { |
apps/server/src/provider/Layers/ProviderService.ts+6-3| ... | ... | @@ -56,6 +56,7 @@ import { |
| 56 | 56 | import { type EventNdjsonLogger } from "./EventNdjsonLogger.ts"; |
| 57 | 57 | import { ProviderEventLoggers } from "./ProviderEventLoggers.ts"; |
| 58 | 58 | import { AnalyticsService } from "../../telemetry/Services/AnalyticsService.ts"; |
| 59 | const isModelSelection = Schema.is(ModelSelection); | |
| 59 | 60 | |
| 60 | 61 | /** |
| 61 | 62 | * Hook for tests that want to override the canonical event logger pulled |
| ... | ... | @@ -87,8 +88,9 @@ const decodeInputOrValidationError = <S extends Schema.Top>(input: { |
| 87 | 88 | readonly operation: string; |
| 88 | 89 | readonly schema: S; |
| 89 | 90 | readonly payload: unknown; |
| 90 | }) => | |
| 91 | Schema.decodeUnknownEffect(input.schema)(input.payload).pipe( | |
| 91 | }) => { | |
| 92 | const decodeProviderRequestInput = Schema.decodeUnknownEffect(input.schema); | |
| 93 | return decodeProviderRequestInput(input.payload).pipe( | |
| 92 | 94 | Effect.mapError( |
| 93 | 95 | (schemaError) => |
| 94 | 96 | new ProviderValidationError({ |
| ... | ... | @@ -98,6 +100,7 @@ const decodeInputOrValidationError = <S extends Schema.Top>(input: { |
| 98 | 100 | }), |
| 99 | 101 | ), |
| 100 | 102 | ); |
| 103 | }; | |
| 101 | 104 | |
| 102 | 105 | function toRuntimeStatus(session: ProviderSession): "starting" | "running" | "stopped" | "error" { |
| 103 | 106 | switch (session.status) { |
| ... | ... | @@ -142,7 +145,7 @@ function readPersistedModelSelection( |
| 142 | 145 | return undefined; |
| 143 | 146 | } |
| 144 | 147 | const raw = "modelSelection" in runtimePayload ? runtimePayload.modelSelection : undefined; |
| 145 | return Schema.is(ModelSelection)(raw) ? raw : undefined; | |
| 148 | return isModelSelection(raw) ? raw : undefined; | |
| 146 | 149 | } |
| 147 | 150 | |
| 148 | 151 | function readPersistedCwd( |
apps/server/src/provider/Layers/ProviderSessionDirectory.ts+2-1| ... | ... | @@ -14,6 +14,7 @@ import { |
| 14 | 14 | type ProviderRuntimeBindingWithMetadata, |
| 15 | 15 | type ProviderSessionDirectoryShape, |
| 16 | 16 | } from "../Services/ProviderSessionDirectory.ts"; |
| 17 | const decodeProviderDriverKindValue = Schema.decodeUnknownEffect(ProviderDriverKind); | |
| 17 | 18 | |
| 18 | 19 | function toPersistenceError(operation: string) { |
| 19 | 20 | return (cause: unknown) => |
| ... | ... | @@ -28,7 +29,7 @@ function decodeProviderDriverKind( |
| 28 | 29 | providerName: string, |
| 29 | 30 | operation: string, |
| 30 | 31 | ): Effect.Effect<ProviderDriverKind, ProviderSessionDirectoryPersistenceError> { |
| 31 | return Schema.decodeUnknownEffect(ProviderDriverKind)(providerName).pipe( | |
| 32 | return decodeProviderDriverKindValue(providerName).pipe( | |
| 32 | 33 | Effect.mapError( |
| 33 | 34 | (cause) => |
| 34 | 35 | new ProviderSessionDirectoryPersistenceError({ |
apps/server/src/provider/acp/AcpAdapterSupport.ts+4-2| ... | ... | @@ -11,6 +11,8 @@ import { |
| 11 | 11 | ProviderAdapterSessionClosedError, |
| 12 | 12 | type ProviderAdapterError, |
| 13 | 13 | } from "../Errors.ts"; |
| 14 | const isAcpProcessExitedError = Schema.is(EffectAcpErrors.AcpProcessExitedError); | |
| 15 | const isAcpRequestError = Schema.is(EffectAcpErrors.AcpRequestError); | |
| 14 | 16 | |
| 15 | 17 | export function mapAcpToAdapterError( |
| 16 | 18 | provider: ProviderDriverKind, |
| ... | ... | @@ -18,14 +20,14 @@ export function mapAcpToAdapterError( |
| 18 | 20 | method: string, |
| 19 | 21 | error: EffectAcpErrors.AcpError, |
| 20 | 22 | ): ProviderAdapterError { |
| 21 | if (Schema.is(EffectAcpErrors.AcpProcessExitedError)(error)) { | |
| 23 | if (isAcpProcessExitedError(error)) { | |
| 22 | 24 | return new ProviderAdapterSessionClosedError({ |
| 23 | 25 | provider, |
| 24 | 26 | threadId, |
| 25 | 27 | cause: error, |
| 26 | 28 | }); |
| 27 | 29 | } |
| 28 | if (Schema.is(EffectAcpErrors.AcpRequestError)(error)) { | |
| 30 | if (isAcpRequestError(error)) { | |
| 29 | 31 | return new ProviderAdapterRequestError({ |
| 30 | 32 | provider, |
| 31 | 33 | method, |
apps/server/src/provider/acp/AcpSessionRuntime.ts+7-4| ... | ... | @@ -7,7 +7,6 @@ import * as Queue from "effect/Queue"; |
| 7 | 7 | import * as Ref from "effect/Ref"; |
| 8 | 8 | import * as Scope from "effect/Scope"; |
| 9 | 9 | import * as Context from "effect/Context"; |
| 10 | import * as Schema from "effect/Schema"; | |
| 11 | 10 | import * as Stream from "effect/Stream"; |
| 12 | 11 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; |
| 13 | 12 | import * as EffectAcpClient from "effect-acp/client"; |
| ... | ... | @@ -27,6 +26,10 @@ import { |
| 27 | 26 | type AcpToolCallState, |
| 28 | 27 | } from "./AcpRuntimeModel.ts"; |
| 29 | 28 | |
| 29 | function formatConfigOptionValue(value: string | boolean): string { | |
| 30 | return JSON.stringify(value); | |
| 31 | } | |
| 32 | ||
| 30 | 33 | export interface AcpSpawnInput { |
| 31 | 34 | readonly command: string; |
| 32 | 35 | readonly args: ReadonlyArray<string>; |
| ... | ... | @@ -277,7 +280,7 @@ const makeAcpSessionRuntime = ( |
| 277 | 280 | } |
| 278 | 281 | return yield* new EffectAcpErrors.AcpRequestError({ |
| 279 | 282 | code: -32602, |
| 280 | errorMessage: `Invalid value ${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(value)} for session config option "${configOption.id}": expected boolean`, | |
| 283 | errorMessage: `Invalid value ${formatConfigOptionValue(value)} for session config option "${configOption.id}": expected boolean`, | |
| 281 | 284 | data: { |
| 282 | 285 | configId: configOption.id, |
| 283 | 286 | expectedType: "boolean", |
| ... | ... | @@ -288,7 +291,7 @@ const makeAcpSessionRuntime = ( |
| 288 | 291 | if (typeof value !== "string") { |
| 289 | 292 | return yield* new EffectAcpErrors.AcpRequestError({ |
| 290 | 293 | code: -32602, |
| 291 | errorMessage: `Invalid value ${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(value)} for session config option "${configOption.id}": expected string`, | |
| 294 | errorMessage: `Invalid value ${formatConfigOptionValue(value)} for session config option "${configOption.id}": expected string`, | |
| 292 | 295 | data: { |
| 293 | 296 | configId: configOption.id, |
| 294 | 297 | expectedType: "string", |
| ... | ... | @@ -302,7 +305,7 @@ const makeAcpSessionRuntime = ( |
| 302 | 305 | } |
| 303 | 306 | return yield* new EffectAcpErrors.AcpRequestError({ |
| 304 | 307 | code: -32602, |
| 305 | errorMessage: `Invalid value ${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(value)} for session config option "${configOption.id}": expected one of ${allowedValues.join(", ")}`, | |
| 308 | errorMessage: `Invalid value ${formatConfigOptionValue(value)} for session config option "${configOption.id}": expected one of ${allowedValues.join(", ")}`, | |
| 306 | 309 | data: { |
| 307 | 310 | configId: configOption.id, |
| 308 | 311 | allowedValues, |
apps/server/src/provider/opencodeRuntime.ts+11-5| ... | ... | @@ -31,6 +31,8 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; |
| 31 | 31 | import { isWindowsCommandNotFound } from "../processRunner.ts"; |
| 32 | 32 | import { collectStreamAsString } from "./providerSnapshot.ts"; |
| 33 | 33 | import * as NetService from "@t3tools/shared/Net"; |
| 34 | const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJsonString); | |
| 35 | const OPENCODE_EMPTY_CONFIG_CONTENT = "{}"; | |
| 34 | 36 | |
| 35 | 37 | const OPENCODE_SERVER_READY_PREFIX = "opencode server listening"; |
| 36 | 38 | const DEFAULT_OPENCODE_SERVER_TIMEOUT_MS = 5_000; |
| ... | ... | @@ -56,6 +58,11 @@ export class OpenCodeRuntimeError extends Data.TaggedError(OPENCODE_RUNTIME_ERRO |
| 56 | 58 | P.isTagged(u, OPENCODE_RUNTIME_ERROR_TAG); |
| 57 | 59 | } |
| 58 | 60 | |
| 61 | function encodeJsonStringForDiagnostics(input: unknown): string | undefined { | |
| 62 | const result = encodeUnknownJsonStringExit(input); | |
| 63 | return Exit.isSuccess(result) ? result.value : undefined; | |
| 64 | } | |
| 65 | ||
| 59 | 66 | export function openCodeRuntimeErrorDetail(cause: unknown): string { |
| 60 | 67 | if (OpenCodeRuntimeError.is(cause)) return cause.detail; |
| 61 | 68 | if (cause instanceof Error && cause.message.trim().length > 0) return cause.message.trim(); |
| ... | ... | @@ -64,10 +71,9 @@ export function openCodeRuntimeErrorDetail(cause: unknown): string { |
| 64 | 71 | const anyCause = cause as Record<string, unknown>; |
| 65 | 72 | const status = (anyCause.response as { status?: number } | undefined)?.status; |
| 66 | 73 | const body = anyCause.error ?? anyCause.data ?? anyCause.body; |
| 67 | try { | |
| 68 | return `status=${status ?? "?"} body=${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(body ?? cause)}`; | |
| 69 | } catch { | |
| 70 | /* fall through */ | |
| 74 | const encodedBody = encodeJsonStringForDiagnostics(body ?? cause); | |
| 75 | if (encodedBody) { | |
| 76 | return `status=${status ?? "?"} body=${encodedBody}`; | |
| 71 | 77 | } |
| 72 | 78 | } |
| 73 | 79 | return String(cause); |
| ... | ... | @@ -336,7 +342,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () { |
| 336 | 342 | shell: process.platform === "win32", |
| 337 | 343 | env: { |
| 338 | 344 | ...(input.environment ?? process.env), |
| 339 | OPENCODE_CONFIG_CONTENT: Schema.encodeUnknownSync(Schema.UnknownFromJsonString)({}), | |
| 345 | OPENCODE_CONFIG_CONTENT: OPENCODE_EMPTY_CONFIG_CONTENT, | |
| 340 | 346 | }, |
| 341 | 347 | }), |
| 342 | 348 | ) |
apps/server/src/provider/providerMaintenanceRunner.test.ts+3-2| ... | ... | @@ -25,6 +25,7 @@ import { |
| 25 | 25 | makeProviderMaintenanceCapabilities, |
| 26 | 26 | type ProviderMaintenanceCapabilities, |
| 27 | 27 | } from "./providerMaintenance.ts"; |
| 28 | const isServerProviderUpdateError = Schema.is(ServerProviderUpdateError); | |
| 28 | 29 | |
| 29 | 30 | const CODEX_DRIVER = ProviderDriverKind.make("codex"); |
| 30 | 31 | const CURSOR_DRIVER = ProviderDriverKind.make("cursor"); |
| ... | ... | @@ -455,8 +456,8 @@ describe("providerMaintenanceRunner", () => { |
| 455 | 456 | assert.strictEqual(Exit.isFailure(second), true); |
| 456 | 457 | if (Exit.isFailure(second)) { |
| 457 | 458 | const error = Cause.squash(second.cause); |
| 458 | assert.strictEqual(Schema.is(ServerProviderUpdateError)(error), true); | |
| 459 | if (Schema.is(ServerProviderUpdateError)(error)) { | |
| 459 | assert.strictEqual(isServerProviderUpdateError(error), true); | |
| 460 | if (isServerProviderUpdateError(error)) { | |
| 460 | 461 | assert.include(error.reason, "already running"); |
| 461 | 462 | } |
| 462 | 463 | } |
apps/server/src/provider/providerMaintenanceRunner.ts+2-1| ... | ... | @@ -25,6 +25,7 @@ import { makeProviderMaintenanceCommandCoordinator } from "./providerMaintenance |
| 25 | 25 | import { enrichProviderSnapshotWithVersionAdvisory } from "./providerMaintenance.ts"; |
| 26 | 26 | import type { ProviderMaintenanceCapabilities } from "./providerMaintenance.ts"; |
| 27 | 27 | import { collectUint8StreamText } from "../stream/collectUint8StreamText.ts"; |
| 28 | const isServerProviderUpdateError = Schema.is(ServerProviderUpdateError); | |
| 28 | 29 | |
| 29 | 30 | const UPDATE_TIMEOUT_MS = 5 * 60_000; |
| 30 | 31 | const UPDATE_OUTPUT_MAX_BYTES = 10_000; |
| ... | ... | @@ -397,7 +398,7 @@ export const make = Effect.fn("ProviderMaintenanceRunner.make")(function* () { |
| 397 | 398 | }) |
| 398 | 399 | .pipe( |
| 399 | 400 | Effect.mapError((error) => |
| 400 | Schema.is(ServerProviderUpdateError)(error) | |
| 401 | isServerProviderUpdateError(error) | |
| 401 | 402 | ? new ServerProviderUpdateError({ |
| 402 | 403 | provider, |
| 403 | 404 | reason: error.reason, |
apps/server/src/serverSettings.ts+5-5| ... | ... | @@ -49,6 +49,7 @@ import { fromLenientJson } from "@t3tools/shared/schemaJson"; |
| 49 | 49 | import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings"; |
| 50 | 50 | import { ServerSecretStoreLive } from "./auth/Layers/ServerSecretStore.ts"; |
| 51 | 51 | import { ServerSecretStore } from "./auth/Services/ServerSecretStore.ts"; |
| 52 | const decodeServerSettings = Schema.decodeEffect(ServerSettings); | |
| 52 | 53 | |
| 53 | 54 | const textEncoder = new TextEncoder(); |
| 54 | 55 | const textDecoder = new TextDecoder(); |
| ... | ... | @@ -127,9 +128,7 @@ export class ServerSettingsService extends Context.Service< |
| 127 | 128 | updateSettings: (patch) => |
| 128 | 129 | Ref.get(currentSettingsRef).pipe( |
| 129 | 130 | Effect.flatMap((currentSettings) => |
| 130 | Schema.decodeEffect(ServerSettings)( | |
| 131 | applyServerSettingsPatch(currentSettings, patch), | |
| 132 | ).pipe( | |
| 131 | decodeServerSettings(applyServerSettingsPatch(currentSettings, patch)).pipe( | |
| 133 | 132 | Effect.mapError( |
| 134 | 133 | (cause) => |
| 135 | 134 | new ServerSettingsError({ |
| ... | ... | @@ -149,6 +148,7 @@ export class ServerSettingsService extends Context.Service< |
| 149 | 148 | } |
| 150 | 149 | |
| 151 | 150 | const ServerSettingsJson = fromLenientJson(ServerSettings); |
| 151 | const decodeServerSettingsJsonExit = Schema.decodeUnknownExit(ServerSettingsJson); | |
| 152 | 152 | |
| 153 | 153 | type LegacyProviderSettings = ServerSettings["providers"][keyof ServerSettings["providers"]]; |
| 154 | 154 | |
| ... | ... | @@ -280,7 +280,7 @@ const makeServerSettings = Effect.gen(function* () { |
| 280 | 280 | } |
| 281 | 281 | |
| 282 | 282 | const raw = yield* readRawConfig; |
| 283 | const decoded = Schema.decodeUnknownExit(ServerSettingsJson)(raw); | |
| 283 | const decoded = decodeServerSettingsJsonExit(raw); | |
| 284 | 284 | if (decoded._tag === "Failure") { |
| 285 | 285 | yield* Effect.logWarning("failed to parse settings.json, using defaults", { |
| 286 | 286 | path: settingsPath, |
| ... | ... | @@ -533,7 +533,7 @@ const makeServerSettings = Effect.gen(function* () { |
| 533 | 533 | current, |
| 534 | 534 | applyServerSettingsPatch(current, patch), |
| 535 | 535 | ); |
| 536 | const next = yield* Schema.decodeEffect(ServerSettings)(nextPersisted).pipe( | |
| 536 | const next = yield* decodeServerSettings(nextPersisted).pipe( | |
| 537 | 537 | Effect.mapError( |
| 538 | 538 | (cause) => |
| 539 | 539 | new ServerSettingsError({ |
apps/server/src/sourceControl/BitbucketApi.ts+2-1| ... | ... | @@ -44,6 +44,7 @@ export class BitbucketApiError extends Schema.TaggedErrorClass<BitbucketApiError |
| 44 | 44 | return `Bitbucket API failed in ${this.operation}: ${this.detail}`; |
| 45 | 45 | } |
| 46 | 46 | } |
| 47 | const isBitbucketApiErrorValue = Schema.is(BitbucketApiError); | |
| 47 | 48 | |
| 48 | 49 | const RawBitbucketRepositorySchema = Schema.Struct({ |
| 49 | 50 | full_name: TrimmedNonEmptyString, |
| ... | ... | @@ -350,7 +351,7 @@ function requestError(operation: string, cause: unknown): BitbucketApiError { |
| 350 | 351 | } |
| 351 | 352 | |
| 352 | 353 | function isBitbucketApiError(cause: unknown): cause is BitbucketApiError { |
| 353 | return Schema.is(BitbucketApiError)(cause); | |
| 354 | return isBitbucketApiErrorValue(cause); | |
| 354 | 355 | } |
| 355 | 356 | |
| 356 | 357 | function responseError( |
apps/server/src/sourceControl/GitHubSourceControlProvider.ts+2-1| ... | ... | @@ -13,6 +13,7 @@ import * as GitHubCli from "./GitHubCli.ts"; |
| 13 | 13 | import * as GitHubPullRequests from "./gitHubPullRequests.ts"; |
| 14 | 14 | import * as SourceControlProvider from "./SourceControlProvider.ts"; |
| 15 | 15 | import * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts"; |
| 16 | const isSourceControlProviderError = Schema.is(SourceControlProviderError); | |
| 16 | 17 | |
| 17 | 18 | function providerError( |
| 18 | 19 | operation: string, |
| ... | ... | @@ -153,7 +154,7 @@ export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () { |
| 153 | 154 | ); |
| 154 | 155 | }), |
| 155 | 156 | Effect.mapError((error) => |
| 156 | Schema.is(SourceControlProviderError)(error) | |
| 157 | isSourceControlProviderError(error) | |
| 157 | 158 | ? error |
| 158 | 159 | : providerError("listChangeRequests", error), |
| 159 | 160 | ), |
apps/server/src/sourceControl/SourceControlRepositoryService.ts+2-1| ... | ... | @@ -22,6 +22,7 @@ import { |
| 22 | 22 | import { ServerConfig } from "../config.ts"; |
| 23 | 23 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; |
| 24 | 24 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; |
| 25 | const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError); | |
| 25 | 26 | |
| 26 | 27 | export interface SourceControlRepositoryServiceShape { |
| 27 | 28 | readonly lookupRepository: ( |
| ... | ... | @@ -69,7 +70,7 @@ function repositoryError(input: { |
| 69 | 70 | |
| 70 | 71 | function mapRepositoryError(operation: string, provider: SourceControlProviderKind) { |
| 71 | 72 | return Effect.mapError((cause: unknown) => |
| 72 | Schema.is(SourceControlRepositoryError)(cause) | |
| 73 | isSourceControlRepositoryError(cause) | |
| 73 | 74 | ? cause |
| 74 | 75 | : repositoryError({ |
| 75 | 76 | operation, |
apps/server/src/textGeneration/ClaudeTextGeneration.test.ts+2-1| ... | ... | @@ -13,6 +13,7 @@ import { ServerConfig } from "../config.ts"; |
| 13 | 13 | import { type TextGenerationShape } from "./TextGeneration.ts"; |
| 14 | 14 | import { sanitizeThreadTitle } from "./TextGenerationUtils.ts"; |
| 15 | 15 | import { makeClaudeTextGeneration } from "./ClaudeTextGeneration.ts"; |
| 16 | const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings); | |
| 16 | 17 | |
| 17 | 18 | const ClaudeTextGenerationTestLayer = ServerConfig.layerTest(process.cwd(), { |
| 18 | 19 | prefix: "t3code-claude-text-generation-test-", |
| ... | ... | @@ -182,7 +183,7 @@ function withFakeClaudeEnv<A, E, R>( |
| 182 | 183 | }), |
| 183 | 184 | ); |
| 184 | 185 | |
| 185 | const config = Schema.decodeSync(ClaudeSettings)(input.claudeConfig ?? {}); | |
| 186 | const config = decodeClaudeSettings(input.claudeConfig ?? {}); | |
| 186 | 187 | const textGeneration = yield* makeClaudeTextGeneration(config); |
| 187 | 188 | return yield* effectFn(textGeneration); |
| 188 | 189 | }).pipe(Effect.scoped); |
apps/server/src/textGeneration/ClaudeTextGeneration.ts+38-8| ... | ... | @@ -53,6 +53,9 @@ const ClaudeOutputEnvelope = Schema.Struct({ |
| 53 | 53 | structured_output: Schema.Unknown, |
| 54 | 54 | }); |
| 55 | 55 | |
| 56 | const encodeJsonString = Schema.encodeEffect(Schema.UnknownFromJsonString); | |
| 57 | const decodeClaudeOutputEnvelope = Schema.decodeEffect(Schema.fromJsonString(ClaudeOutputEnvelope)); | |
| 58 | ||
| 56 | 59 | export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(function* ( |
| 57 | 60 | claudeSettings: ClaudeSettings, |
| 58 | 61 | environment: NodeJS.ProcessEnv = process.env, |
| ... | ... | @@ -75,6 +78,26 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu |
| 75 | 78 | ), |
| 76 | 79 | ); |
| 77 | 80 | |
| 81 | const encodeJsonForOperation = ( | |
| 82 | operation: | |
| 83 | | "generateCommitMessage" | |
| 84 | | "generatePrContent" | |
| 85 | | "generateBranchName" | |
| 86 | | "generateThreadTitle", | |
| 87 | value: unknown, | |
| 88 | detail: string, | |
| 89 | ): Effect.Effect<string, TextGenerationError> => | |
| 90 | encodeJsonString(value).pipe( | |
| 91 | Effect.mapError( | |
| 92 | (cause) => | |
| 93 | new TextGenerationError({ | |
| 94 | operation, | |
| 95 | detail, | |
| 96 | cause, | |
| 97 | }), | |
| 98 | ), | |
| 99 | ); | |
| 100 | ||
| 78 | 101 | /** |
| 79 | 102 | * Spawn the Claude CLI with structured JSON output and return the parsed, |
| 80 | 103 | * schema-validated result. |
| ... | ... | @@ -96,8 +119,10 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu |
| 96 | 119 | outputSchemaJson: S; |
| 97 | 120 | modelSelection: ModelSelection; |
| 98 | 121 | }): Effect.fn.Return<S["Type"], TextGenerationError, S["DecodingServices"]> { |
| 99 | const jsonSchemaStr = Schema.encodeUnknownSync(Schema.UnknownFromJsonString)( | |
| 122 | const jsonSchemaStr = yield* encodeJsonForOperation( | |
| 123 | operation, | |
| 100 | 124 | toJsonSchemaObject(outputSchemaJson), |
| 125 | "Failed to encode structured output schema.", | |
| 101 | 126 | ); |
| 102 | 127 | const caps = getClaudeModelCapabilities(modelSelection.model); |
| 103 | 128 | const descriptors = getProviderOptionDescriptors({ |
| ... | ... | @@ -118,6 +143,14 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu |
| 118 | 143 | ...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}), |
| 119 | 144 | ...(fastMode ? { fastMode: true } : {}), |
| 120 | 145 | }; |
| 146 | const settingsJson = | |
| 147 | Object.keys(settings).length > 0 | |
| 148 | ? yield* encodeJsonForOperation( | |
| 149 | operation, | |
| 150 | settings, | |
| 151 | "Failed to encode Claude CLI settings.", | |
| 152 | ) | |
| 153 | : undefined; | |
| 121 | 154 | |
| 122 | 155 | const runClaudeCommand = Effect.fn("runClaudeJson.runClaudeCommand")(function* () { |
| 123 | 156 | const command = ChildProcess.make( |
| ... | ... | @@ -131,9 +164,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu |
| 131 | 164 | "--model", |
| 132 | 165 | resolveClaudeApiModelId(modelSelection), |
| 133 | 166 | ...(cliEffort ? ["--effort", cliEffort] : []), |
| 134 | ...(Object.keys(settings).length > 0 | |
| 135 | ? ["--settings", Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(settings)] | |
| 136 | : []), | |
| 167 | ...(settingsJson ? ["--settings", settingsJson] : []), | |
| 137 | 168 | "--dangerously-skip-permissions", |
| 138 | 169 | ], |
| 139 | 170 | { |
| ... | ... | @@ -197,9 +228,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu |
| 197 | 228 | ), |
| 198 | 229 | ); |
| 199 | 230 | |
| 200 | const envelope = yield* Schema.decodeEffect(Schema.fromJsonString(ClaudeOutputEnvelope))( | |
| 201 | rawStdout, | |
| 202 | ).pipe( | |
| 231 | const envelope = yield* decodeClaudeOutputEnvelope(rawStdout).pipe( | |
| 203 | 232 | Effect.catchTag("SchemaError", (cause) => |
| 204 | 233 | Effect.fail( |
| 205 | 234 | new TextGenerationError({ |
| ... | ... | @@ -211,7 +240,8 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu |
| 211 | 240 | ), |
| 212 | 241 | ); |
| 213 | 242 | |
| 214 | return yield* Schema.decodeEffect(outputSchemaJson)(envelope.structured_output).pipe( | |
| 243 | const decodeOutput = Schema.decodeEffect(outputSchemaJson); | |
| 244 | return yield* decodeOutput(envelope.structured_output).pipe( | |
| 215 | 245 | Effect.catchTag("SchemaError", (cause) => |
| 216 | 246 | Effect.fail( |
| 217 | 247 | new TextGenerationError({ |
apps/server/src/textGeneration/CodexTextGeneration.test.ts+2-1| ... | ... | @@ -14,6 +14,7 @@ import { CodexSettings, ProviderInstanceId, TextGenerationError } from "@t3tools |
| 14 | 14 | import { ServerConfig } from "../config.ts"; |
| 15 | 15 | import { type TextGenerationShape } from "./TextGeneration.ts"; |
| 16 | 16 | import { makeCodexTextGeneration } from "./CodexTextGeneration.ts"; |
| 17 | const decodeCodexSettings = Schema.decodeSync(CodexSettings); | |
| 17 | 18 | |
| 18 | 19 | const DEFAULT_TEST_MODEL_SELECTION = createModelSelection( |
| 19 | 20 | ProviderInstanceId.make("codex"), |
| ... | ... | @@ -172,7 +173,7 @@ function withFakeCodexEnv<A, E, R>( |
| 172 | 173 | const fs = yield* FileSystem.FileSystem; |
| 173 | 174 | const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-codex-text-" }); |
| 174 | 175 | const codexPath = yield* makeFakeCodexBinary(tempDir, input); |
| 175 | const config = Schema.decodeSync(CodexSettings)({ binaryPath: codexPath }); | |
| 176 | const config = decodeCodexSettings({ binaryPath: codexPath }); | |
| 176 | 177 | const textGeneration = yield* makeCodexTextGeneration(config); |
| 177 | 178 | return yield* effectFn(textGeneration); |
| 178 | 179 | }).pipe(Effect.scoped); |
apps/server/src/textGeneration/CodexTextGeneration.ts+26-4| ... | ... | @@ -40,6 +40,7 @@ import { |
| 40 | 40 | |
| 41 | 41 | const CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT = "low"; |
| 42 | 42 | const CODEX_TIMEOUT_MS = 180_000; |
| 43 | const encodeJsonString = Schema.encodeEffect(Schema.UnknownFromJsonString); | |
| 43 | 44 | /** |
| 44 | 45 | * Build a Codex text-generation closure bound to a specific `CodexSettings` |
| 45 | 46 | * payload. See `makeCodexAdapter` for the overall per-instance rationale. |
| ... | ... | @@ -99,6 +100,25 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func |
| 99 | 100 | const safeUnlink = (filePath: string): Effect.Effect<void, never> => |
| 100 | 101 | fileSystem.remove(filePath).pipe(Effect.catch(() => Effect.void)); |
| 101 | 102 | |
| 103 | const encodeJsonForOperation = ( | |
| 104 | operation: | |
| 105 | | "generateCommitMessage" | |
| 106 | | "generatePrContent" | |
| 107 | | "generateBranchName" | |
| 108 | | "generateThreadTitle", | |
| 109 | value: unknown, | |
| 110 | ): Effect.Effect<string, TextGenerationError> => | |
| 111 | encodeJsonString(value).pipe( | |
| 112 | Effect.mapError( | |
| 113 | (cause) => | |
| 114 | new TextGenerationError({ | |
| 115 | operation, | |
| 116 | detail: "Failed to encode structured output schema.", | |
| 117 | cause, | |
| 118 | }), | |
| 119 | ), | |
| 120 | ); | |
| 121 | ||
| 102 | 122 | const materializeImageAttachments = Effect.fn("materializeImageAttachments")(function* ( |
| 103 | 123 | _operation: |
| 104 | 124 | | "generateCommitMessage" |
| ... | ... | @@ -156,11 +176,11 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func |
| 156 | 176 | cleanupPaths?: ReadonlyArray<string>; |
| 157 | 177 | modelSelection: ModelSelection; |
| 158 | 178 | }): Effect.fn.Return<S["Type"], TextGenerationError, S["DecodingServices"]> { |
| 159 | const schemaPath = yield* writeTempFile( | |
| 179 | const schemaJson = yield* encodeJsonForOperation( | |
| 160 | 180 | operation, |
| 161 | "codex-schema", | |
| 162 | Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(toJsonSchemaObject(outputSchemaJson)), | |
| 181 | toJsonSchemaObject(outputSchemaJson), | |
| 163 | 182 | ); |
| 183 | const schemaPath = yield* writeTempFile(operation, "codex-schema", schemaJson); | |
| 164 | 184 | const outputPath = yield* writeTempFile(operation, "codex-output", ""); |
| 165 | 185 | |
| 166 | 186 | const runCodexCommand = Effect.fn("runCodexJson.runCodexCommand")(function* () { |
| ... | ... | @@ -259,6 +279,8 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func |
| 259 | 279 | ), |
| 260 | 280 | ); |
| 261 | 281 | |
| 282 | const decodeOutput = Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson)); | |
| 283 | ||
| 262 | 284 | return yield* fileSystem.readFileString(outputPath).pipe( |
| 263 | 285 | Effect.mapError( |
| 264 | 286 | (cause) => |
| ... | ... | @@ -268,7 +290,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func |
| 268 | 290 | cause, |
| 269 | 291 | }), |
| 270 | 292 | ), |
| 271 | Effect.flatMap(Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson))), | |
| 293 | Effect.flatMap(decodeOutput), | |
| 272 | 294 | Effect.catchTag("SchemaError", (cause) => |
| 273 | 295 | Effect.fail( |
| 274 | 296 | new TextGenerationError({ |
apps/server/src/textGeneration/CursorTextGeneration.test.ts+2-1| ... | ... | @@ -19,6 +19,7 @@ import { CursorSettings, ProviderInstanceId } from "@t3tools/contracts"; |
| 19 | 19 | import { ServerConfig } from "../config.ts"; |
| 20 | 20 | import { type TextGenerationShape } from "./TextGeneration.ts"; |
| 21 | 21 | import { makeCursorTextGeneration } from "./CursorTextGeneration.ts"; |
| 22 | const decodeCursorSettings = Schema.decodeSync(CursorSettings); | |
| 22 | 23 | |
| 23 | 24 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| 24 | 25 | const mockAgentPath = path.join(__dirname, "../../scripts/acp-mock-agent.ts"); |
| ... | ... | @@ -65,7 +66,7 @@ function withFakeAcpAgent<A, E, R>( |
| 65 | 66 | }), |
| 66 | 67 | ); |
| 67 | 68 | const agentPath = makeAcpAgentWrapper(tempDir, env); |
| 68 | const config = Schema.decodeSync(CursorSettings)({ binaryPath: agentPath }); | |
| 69 | const config = decodeCursorSettings({ binaryPath: agentPath }); | |
| 69 | 70 | const textGeneration = yield* makeCursorTextGeneration(config); |
| 70 | 71 | return yield* effectFn(textGeneration); |
| 71 | 72 | }).pipe(Effect.scoped); |
apps/server/src/textGeneration/CursorTextGeneration.ts+2-3| ... | ... | @@ -154,9 +154,8 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu |
| 154 | 154 | }); |
| 155 | 155 | } |
| 156 | 156 | |
| 157 | return yield* Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson))( | |
| 158 | extractJsonObject(rawResult), | |
| 159 | ).pipe( | |
| 157 | const decodeOutput = Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson)); | |
| 158 | return yield* decodeOutput(extractJsonObject(rawResult)).pipe( | |
| 160 | 159 | Effect.catchTag("SchemaError", (cause) => |
| 161 | 160 | Effect.fail( |
| 162 | 161 | new TextGenerationError({ |
apps/server/src/textGeneration/OpenCodeTextGeneration.ts+2-3| ... | ... | @@ -352,9 +352,8 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" |
| 352 | 352 | releaseSharedServer, |
| 353 | 353 | ); |
| 354 | 354 | |
| 355 | return yield* Schema.decodeEffect(Schema.fromJsonString(input.outputSchemaJson))( | |
| 356 | extractJsonObject(rawOutput), | |
| 357 | ).pipe( | |
| 355 | const decodeOutput = Schema.decodeEffect(Schema.fromJsonString(input.outputSchemaJson)); | |
| 356 | return yield* decodeOutput(extractJsonObject(rawOutput)).pipe( | |
| 358 | 357 | Effect.catchTag("SchemaError", (cause) => |
| 359 | 358 | Effect.fail( |
| 360 | 359 | new TextGenerationError({ |
apps/server/src/textGeneration/TextGenerationUtils.ts+2-1| ... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 | import * as Schema from "effect/Schema"; |
| 2 | 2 | |
| 3 | 3 | import { TextGenerationError } from "@t3tools/contracts"; |
| 4 | const isTextGenerationError = Schema.is(TextGenerationError); | |
| 4 | 5 | |
| 5 | 6 | /** Convert an Effect Schema to a flat JSON Schema object, inlining `$defs` when present. */ |
| 6 | 7 | export function toJsonSchemaObject(schema: Schema.Top): unknown { |
| ... | ... | @@ -127,7 +128,7 @@ export function normalizeCliError( |
| 127 | 128 | error: unknown, |
| 128 | 129 | fallback: string, |
| 129 | 130 | ): TextGenerationError { |
| 130 | if (Schema.is(TextGenerationError)(error)) { | |
| 131 | if (isTextGenerationError(error)) { | |
| 131 | 132 | return error; |
| 132 | 133 | } |
| 133 | 134 |
apps/server/src/vcs/GitVcsDriverCore.ts+2-1| ... | ... | @@ -28,6 +28,7 @@ import { |
| 28 | 28 | parseRemoteRefWithRemoteNames, |
| 29 | 29 | } from "../git/remoteRefs.ts"; |
| 30 | 30 | import { ServerConfig } from "../config.ts"; |
| 31 | const isGitCommandError = Schema.is(GitCommandError); | |
| 31 | 32 | |
| 32 | 33 | const DEFAULT_TIMEOUT_MS = 30_000; |
| 33 | 34 | const DEFAULT_MAX_OUTPUT_BYTES = 1_000_000; |
| ... | ... | @@ -327,7 +328,7 @@ function toGitCommandError( |
| 327 | 328 | detail: string, |
| 328 | 329 | ) { |
| 329 | 330 | return (cause: unknown) => |
| 330 | Schema.is(GitCommandError)(cause) | |
| 331 | isGitCommandError(cause) | |
| 331 | 332 | ? cause |
| 332 | 333 | : new GitCommandError({ |
| 333 | 334 | operation: input.operation, |
apps/server/src/vcs/VcsProjectConfig.ts+2-1| ... | ... | @@ -15,6 +15,7 @@ const ProjectVcsConfig = Schema.Struct({ |
| 15 | 15 | ), |
| 16 | 16 | vcsKind: Schema.optional(VcsDriverKind), |
| 17 | 17 | }); |
| 18 | const isProjectVcsConfig = Schema.is(ProjectVcsConfig); | |
| 18 | 19 | |
| 19 | 20 | interface ProjectVcsConfigFile { |
| 20 | 21 | readonly vcs?: |
| ... | ... | @@ -47,7 +48,7 @@ function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto |
| 47 | 48 | function parseConfig(raw: string): ProjectVcsConfigFile | null { |
| 48 | 49 | try { |
| 49 | 50 | const parsed = JSON.parse(raw) as unknown; |
| 50 | return Schema.is(ProjectVcsConfig)(parsed) ? parsed : null; | |
| 51 | return isProjectVcsConfig(parsed) ? parsed : null; | |
| 51 | 52 | } catch { |
| 52 | 53 | return null; |
| 53 | 54 | } |
apps/server/src/ws.ts+6-4| ... | ... | @@ -86,6 +86,8 @@ import { |
| 86 | 86 | type SessionCredentialChange, |
| 87 | 87 | } from "./auth/Services/SessionCredentialService.ts"; |
| 88 | 88 | import { respondToAuthError } from "./auth/http.ts"; |
| 89 | const isOrchestrationDispatchCommandError = Schema.is(OrchestrationDispatchCommandError); | |
| 90 | const isWorkspacePathOutsideRootError = Schema.is(WorkspacePathOutsideRootError); | |
| 89 | 91 | |
| 90 | 92 | const nowIso = Effect.map(DateTime.now, DateTime.formatIso); |
| 91 | 93 | |
| ... | ... | @@ -216,7 +218,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) => |
| 216 | 218 | }); |
| 217 | 219 | |
| 218 | 220 | const toDispatchCommandError = (cause: unknown, fallbackMessage: string) => |
| 219 | Schema.is(OrchestrationDispatchCommandError)(cause) | |
| 221 | isOrchestrationDispatchCommandError(cause) | |
| 220 | 222 | ? cause |
| 221 | 223 | : new OrchestrationDispatchCommandError({ |
| 222 | 224 | message: cause instanceof Error ? cause.message : fallbackMessage, |
| ... | ... | @@ -225,7 +227,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) => |
| 225 | 227 | |
| 226 | 228 | const toBootstrapDispatchCommandCauseError = (cause: Cause.Cause<unknown>) => { |
| 227 | 229 | const error = Cause.squash(cause); |
| 228 | return Schema.is(OrchestrationDispatchCommandError)(error) | |
| 230 | return isOrchestrationDispatchCommandError(error) | |
| 229 | 231 | ? error |
| 230 | 232 | : new OrchestrationDispatchCommandError({ |
| 231 | 233 | message: |
| ... | ... | @@ -637,7 +639,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) => |
| 637 | 639 | return result; |
| 638 | 640 | }).pipe( |
| 639 | 641 | Effect.mapError((cause) => |
| 640 | Schema.is(OrchestrationDispatchCommandError)(cause) | |
| 642 | isOrchestrationDispatchCommandError(cause) | |
| 641 | 643 | ? cause |
| 642 | 644 | : new OrchestrationDispatchCommandError({ |
| 643 | 645 | message: "Failed to dispatch orchestration command", |
| ... | ... | @@ -918,7 +920,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) => |
| 918 | 920 | WS_METHODS.projectsWriteFile, |
| 919 | 921 | workspaceFileSystem.writeFile(input).pipe( |
| 920 | 922 | Effect.mapError((cause) => { |
| 921 | const message = Schema.is(WorkspacePathOutsideRootError)(cause) | |
| 923 | const message = isWorkspacePathOutsideRootError(cause) | |
| 922 | 924 | ? "Workspace file path must stay within the project root." |
| 923 | 925 | : "Failed to write workspace file"; |
| 924 | 926 | return new ProjectWriteFileError({ |
apps/web/src/components/settings/ProviderSettingsForm.tsx+4-6| ... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 | "use client"; |
| 2 | 2 | |
| 3 | 3 | import { useMemo, type ReactNode } from "react"; |
| 4 | import * as Option from "effect/Option"; | |
| 4 | 5 | import * as Schema from "effect/Schema"; |
| 5 | 6 | import type { |
| 6 | 7 | ProviderSettingsFormAnnotation, |
| ... | ... | @@ -63,12 +64,9 @@ function readProviderSettingsFormSchemaAnnotation( |
| 63 | 64 | function readFieldBooleanDefault( |
| 64 | 65 | fieldSchema: ProviderClientDefinition["settingsSchema"]["fields"][string], |
| 65 | 66 | ): boolean | undefined { |
| 66 | try { | |
| 67 | const decoded = Schema.decodeUnknownSync(fieldSchema as Schema.Decoder<unknown>)(undefined); | |
| 68 | return typeof decoded === "boolean" ? decoded : undefined; | |
| 69 | } catch { | |
| 70 | return undefined; | |
| 71 | } | |
| 67 | const decodeDefault = Schema.decodeUnknownOption(fieldSchema as Schema.Decoder<unknown>); | |
| 68 | const decoded = decodeDefault(undefined); | |
| 69 | return Option.isSome(decoded) && typeof decoded.value === "boolean" ? decoded.value : undefined; | |
| 72 | 70 | } |
| 73 | 71 | |
| 74 | 72 | export function deriveProviderSettingsFields( |
apps/web/src/composerDraftStore.ts+3-2| ... | ... | @@ -42,11 +42,12 @@ import { useShallow } from "zustand/react/shallow"; |
| 42 | 42 | import { createDebouncedStorage, createMemoryStorage } from "./lib/storage"; |
| 43 | 43 | import { getDefaultServerModel } from "./providerModels"; |
| 44 | 44 | import { UnifiedSettings } from "@t3tools/contracts/settings"; |
| 45 | const isRuntimeMode = Schema.is(RuntimeMode); | |
| 46 | const isProviderDriverKind = Schema.is(ProviderDriverKind); | |
| 45 | 47 | |
| 46 | 48 | export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1"; |
| 47 | 49 | const COMPOSER_DRAFT_STORAGE_VERSION = 6; |
| 48 | 50 | const DraftThreadEnvModeSchema = Schema.Literals(["local", "worktree"]); |
| 49 | const isRuntimeMode = Schema.is(RuntimeMode); | |
| 50 | 51 | export type DraftThreadEnvMode = typeof DraftThreadEnvModeSchema.Type; |
| 51 | 52 | |
| 52 | 53 | export const DraftId = Schema.String.pipe(Schema.brand("DraftId")); |
| ... | ... | @@ -575,7 +576,7 @@ function shouldRemoveDraft(draft: ComposerThreadDraftState): boolean { |
| 575 | 576 | } |
| 576 | 577 | |
| 577 | 578 | function normalizeProviderDriverKind(value: unknown): ProviderDriverKind | null { |
| 578 | return Schema.is(ProviderDriverKind)(value) ? value : null; | |
| 579 | return isProviderDriverKind(value) ? value : null; | |
| 579 | 580 | } |
| 580 | 581 | |
| 581 | 582 | /** |
apps/web/src/hooks/useLocalStorage.ts+8-4| ... | ... | @@ -19,11 +19,15 @@ const isomorphicLocalStorage: Storage = |
| 19 | 19 | }; |
| 20 | 20 | })(); |
| 21 | 21 | |
| 22 | const decode = <T, E>(schema: Schema.Codec<T, E>, value: string) => | |
| 23 | Schema.decodeSync(Schema.fromJsonString(schema))(value); | |
| 22 | const decode = <T, E>(schema: Schema.Codec<T, E>, value: string) => { | |
| 23 | const decodeJson = Schema.decodeSync(Schema.fromJsonString(schema)); | |
| 24 | return decodeJson(value); | |
| 25 | }; | |
| 24 | 26 | |
| 25 | const encode = <T, E>(schema: Schema.Codec<T, E>, value: T) => | |
| 26 | Schema.encodeSync(Schema.fromJsonString(schema))(value); | |
| 27 | const encode = <T, E>(schema: Schema.Codec<T, E>, value: T) => { | |
| 28 | const encodeJson = Schema.encodeSync(Schema.fromJsonString(schema)); | |
| 29 | return encodeJson(value); | |
| 30 | }; | |
| 27 | 31 | |
| 28 | 32 | export const getLocalStorageItem = <T, E>(key: string, schema: Schema.Codec<T, E>): T | null => { |
| 29 | 33 | const item = isomorphicLocalStorage.getItem(key); |
apps/web/src/lib/projectScriptKeybindings.ts+3-1| ... | ... | @@ -8,6 +8,8 @@ import * as Schema from "effect/Schema"; |
| 8 | 8 | |
| 9 | 9 | export const PROJECT_SCRIPT_KEYBINDING_INVALID_MESSAGE = "Invalid keybinding."; |
| 10 | 10 | |
| 11 | const decodeKeybindingRule = Schema.decodeUnknownOption(KeybindingRuleSchema); | |
| 12 | ||
| 11 | 13 | function normalizeProjectScriptKeybindingInput( |
| 12 | 14 | keybinding: string | null | undefined, |
| 13 | 15 | ): string | null { |
| ... | ... | @@ -22,7 +24,7 @@ export function decodeProjectScriptKeybindingRule(input: { |
| 22 | 24 | const normalizedKey = normalizeProjectScriptKeybindingInput(input.keybinding); |
| 23 | 25 | if (!normalizedKey) return null; |
| 24 | 26 | |
| 25 | const decoded = Schema.decodeUnknownOption(KeybindingRuleSchema)({ | |
| 27 | const decoded = decodeKeybindingRule({ | |
| 26 | 28 | key: normalizedKey, |
| 27 | 29 | command: input.command, |
| 28 | 30 | }); |
apps/web/src/lib/providerReactQuery.ts+5-2| ... | ... | @@ -9,6 +9,9 @@ import * as Option from "effect/Option"; |
| 9 | 9 | import * as Schema from "effect/Schema"; |
| 10 | 10 | import { ensureEnvironmentApi } from "../environmentApi"; |
| 11 | 11 | |
| 12 | const decodeFullThreadDiffInput = Schema.decodeUnknownOption(OrchestrationGetFullThreadDiffInput); | |
| 13 | const decodeTurnDiffInput = Schema.decodeUnknownOption(OrchestrationGetTurnDiffInput); | |
| 14 | ||
| 12 | 15 | interface CheckpointDiffQueryInput { |
| 13 | 16 | environmentId: EnvironmentId | null; |
| 14 | 17 | threadId: ThreadId | null; |
| ... | ... | @@ -36,14 +39,14 @@ export const providerQueryKeys = { |
| 36 | 39 | |
| 37 | 40 | function decodeCheckpointDiffRequest(input: CheckpointDiffQueryInput) { |
| 38 | 41 | if (input.fromTurnCount === 0) { |
| 39 | return Schema.decodeUnknownOption(OrchestrationGetFullThreadDiffInput)({ | |
| 42 | return decodeFullThreadDiffInput({ | |
| 40 | 43 | threadId: input.threadId, |
| 41 | 44 | toTurnCount: input.toTurnCount, |
| 42 | 45 | ignoreWhitespace: input.ignoreWhitespace, |
| 43 | 46 | }).pipe(Option.map((fields) => ({ kind: "fullThreadDiff" as const, input: fields }))); |
| 44 | 47 | } |
| 45 | 48 | |
| 46 | return Schema.decodeUnknownOption(OrchestrationGetTurnDiffInput)({ | |
| 49 | return decodeTurnDiffInput({ | |
| 47 | 50 | threadId: input.threadId, |
| 48 | 51 | fromTurnCount: input.fromTurnCount, |
| 49 | 52 | toTurnCount: input.toTurnCount, |
apps/web/src/projectScripts.ts+2-1| ... | ... | @@ -5,6 +5,7 @@ import { |
| 5 | 5 | type ProjectScript, |
| 6 | 6 | } from "@t3tools/contracts"; |
| 7 | 7 | import * as Schema from "effect/Schema"; |
| 8 | const isScriptRunCommand = Schema.is(SCRIPT_RUN_COMMAND_PATTERN); | |
| 8 | 9 | |
| 9 | 10 | function normalizeScriptId(value: string): string { |
| 10 | 11 | const cleaned = value |
| ... | ... | @@ -26,7 +27,7 @@ export const commandForProjectScript = (scriptId: string): KeybindingCommand => |
| 26 | 27 | |
| 27 | 28 | export function projectScriptIdFromCommand(command: string): string | null { |
| 28 | 29 | const trimmed = command.trim(); |
| 29 | if (!Schema.is(SCRIPT_RUN_COMMAND_PATTERN)(trimmed)) { | |
| 30 | if (!isScriptRunCommand(trimmed)) { | |
| 30 | 31 | return null; |
| 31 | 32 | } |
| 32 | 33 | const [prefix, , suffix] = SCRIPT_RUN_COMMAND_PATTERN.parts; |
apps/web/src/store.ts+2-1| ... | ... | @@ -37,6 +37,7 @@ import { |
| 37 | 37 | import { resolveEnvironmentHttpUrl } from "./environments/runtime"; |
| 38 | 38 | import { sanitizeThreadErrorMessage } from "./rpc/transportError"; |
| 39 | 39 | import { getThreadFromEnvironmentState } from "./threadDerivation"; |
| 40 | const isProviderDriverKindValue = Schema.is(ProviderDriverKind); | |
| 40 | 41 | |
| 41 | 42 | export interface EnvironmentState { |
| 42 | 43 | projectIds: ProjectId[]; |
| ... | ... | @@ -1006,7 +1007,7 @@ function toLegacySessionStatus( |
| 1006 | 1007 | } |
| 1007 | 1008 | |
| 1008 | 1009 | function toLegacyProvider(providerName: string | null): ProviderDriverKind { |
| 1009 | if (Schema.is(ProviderDriverKind)(providerName)) { | |
| 1010 | if (isProviderDriverKindValue(providerName)) { | |
| 1010 | 1011 | return providerName; |
| 1011 | 1012 | } |
| 1012 | 1013 | return ProviderDriverKind.make("codex"); |
apps/web/src/uiStateStore.test.ts+20-20| ... | ... | @@ -445,27 +445,27 @@ describe("uiStateStore pure functions", () => { |
| 445 | 445 | }); |
| 446 | 446 | }); |
| 447 | 447 | |
| 448 | describe("uiStateStore persistence round-trip", () => { | |
| 449 | function createLocalStorageStub(): Storage { | |
| 450 | const store = new Map<string, string>(); | |
| 451 | return { | |
| 452 | clear: () => { | |
| 453 | store.clear(); | |
| 454 | }, | |
| 455 | getItem: (key) => store.get(key) ?? null, | |
| 456 | key: (index) => [...store.keys()][index] ?? null, | |
| 457 | get length() { | |
| 458 | return store.size; | |
| 459 | }, | |
| 460 | removeItem: (key) => { | |
| 461 | store.delete(key); | |
| 462 | }, | |
| 463 | setItem: (key, value) => { | |
| 464 | store.set(key, value); | |
| 465 | }, | |
| 466 | }; | |
| 467 | } | |
| 448 | function createLocalStorageStub(): Storage { | |
| 449 | const store = new Map<string, string>(); | |
| 450 | return { | |
| 451 | clear: () => { | |
| 452 | store.clear(); | |
| 453 | }, | |
| 454 | getItem: (key) => store.get(key) ?? null, | |
| 455 | key: (index) => [...store.keys()][index] ?? null, | |
| 456 | get length() { | |
| 457 | return store.size; | |
| 458 | }, | |
| 459 | removeItem: (key) => { | |
| 460 | store.delete(key); | |
| 461 | }, | |
| 462 | setItem: (key, value) => { | |
| 463 | store.set(key, value); | |
| 464 | }, | |
| 465 | }; | |
| 466 | } | |
| 468 | 467 | |
| 468 | describe("uiStateStore persistence round-trip", () => { | |
| 469 | 469 | let localStorageStub: Storage; |
| 470 | 470 | |
| 471 | 471 | beforeEach(() => { |
bun.lock+41-21| ... | ... | @@ -6,9 +6,10 @@ |
| 6 | 6 | "name": "@t3tools/monorepo", |
| 7 | 7 | "devDependencies": { |
| 8 | 8 | "@effect/language-service": "catalog:", |
| 9 | "@oxlint/plugins": "^1.63.0", | |
| 9 | 10 | "@types/node": "catalog:", |
| 10 | 11 | "oxfmt": "^0.40.0", |
| 11 | "oxlint": "^1.55.0", | |
| 12 | "oxlint": "^1.63.0", | |
| 12 | 13 | "turbo": "^2.3.3", |
| 13 | 14 | "typescript": "catalog:", |
| 14 | 15 | "vitest": "catalog:", |
| ... | ... | @@ -135,6 +136,21 @@ |
| 135 | 136 | "vitest-browser-react": "^2.0.5", |
| 136 | 137 | }, |
| 137 | 138 | }, |
| 139 | "oxlint-plugin-t3code": { | |
| 140 | "name": "@t3tools/oxlint-plugin-t3code", | |
| 141 | "dependencies": { | |
| 142 | "@effect/platform-node": "catalog:", | |
| 143 | "@oxlint/plugins": "^1.63.0", | |
| 144 | "effect": "catalog:", | |
| 145 | }, | |
| 146 | "devDependencies": { | |
| 147 | "@effect/language-service": "catalog:", | |
| 148 | "@effect/vitest": "catalog:", | |
| 149 | "@types/bun": "catalog:", | |
| 150 | "typescript": "catalog:", | |
| 151 | "vitest": "catalog:", | |
| 152 | }, | |
| 153 | }, | |
| 138 | 154 | "packages/client-runtime": { |
| 139 | 155 | "name": "@t3tools/client-runtime", |
| 140 | 156 | "version": "0.0.0-alpha.1", |
| ... | ... | @@ -667,43 +683,45 @@ |
| 667 | 683 | |
| 668 | 684 | "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/Zmj0yTYSvmha6TG1QnoLqVT7ZMRDqXvFXXBQpIjteEwx9qvUYMBH2xbiOFhDeMUJkGwC3D6fdKsFtaqUvkwNA=="], |
| 669 | 685 | |
| 670 | "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-IyfYPthZyiSKwAv/dLjeO18SaK8MxLI9Yss2JrRDyweQAkuL3LhEy7pwIwI7uA3KQc1Vdn20kdmj3q0oUIQL6A=="], | |
| 686 | "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.63.0", "", { "os": "android", "cpu": "arm" }, "sha512-A9xLtQt7i0OA1PoB/meog6kikXI9CdwEp7ZwQqmgnpKn3G3b1orvTDy8CQ6T7w1HvDrgWGB78PkFKcWgibcTCg=="], | |
| 687 | ||
| 688 | "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.63.0", "", { "os": "android", "cpu": "arm64" }, "sha512-SQo+ZMvdR9l3CxZp5W5gFNxSiDxclY6lOzzNpKYLF8asESpm3Pwumx0gER5T7aHLF1/2BAAtLD3DiDkdgy4V1A=="], | |
| 671 | 689 | |
| 672 | "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Ga5zYrzH6vc/VFxhn6MmyUnYEfy9vRpwTIks99mY3j6Nz30yYpIkWryI0QKPCgvGUtDSXVLEaMum5nA+WrNOSg=="], | |
| 690 | "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.63.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6W82XjJDTmMnjg30427l0dufpnyLoq7wEukKdM6/g2VIybRVuQiBVh43EA4b+UxZ3+tLcKm+Or/pXGNgLCEU8g=="], | |
| 673 | 691 | |
| 674 | "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ogmbdJysnw/D4bDcpf1sPLpFThZ48lYp4aKYm10Z/6Nh1SON6NtnNhTNOlhEY296tDFItsZUz+2tgcSYqh8Eyw=="], | |
| 692 | "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.63.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CnWd/YCuVG5W1BYkjJEVbJG11o526O9qAwBEQM+nh8K19CRFUkFdROXCyYkGmroHEYQe4vgQ6+lh3550Lp35Xw=="], | |
| 675 | 693 | |
| 676 | "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-x8QE1h+RAtQ2g+3KPsP6Fk/tdz6zJQUv5c7fTrJxXV3GHOo+Ry5p/PsogU4U+iUZg0rj6hS+E4xi+mnwwlDCWQ=="], | |
| 694 | "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.63.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-a4eZAqrmtajqcxfdAzC+l7g3PaE3V8hpAYqqeD3fTxLXOMFdK3eNTZrU80n4dDEVm0JXy1aL5PqvqWldBl6zYA=="], | |
| 677 | 695 | |
| 678 | "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6G+WMZvwJpMvY7my+/SHEjb7BTk/PFbePqLpmVmUJRIsJMy/UlyYqjpuh0RCgYYkPLcnXm1rUM04kbTk8yS1Yg=="], | |
| 696 | "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.63.0", "", { "os": "linux", "cpu": "arm" }, "sha512-tYUtU9TdbU3uXF5D62g5zXJ13iniFGhXQx5vp9cyEjGdbSAY3VdFBSaldYvyoDmgMZ0ZYuwQP1Y4t2Fhejwa0w=="], | |
| 679 | 697 | |
| 680 | "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-YYHBsk/sl7fYwQOok+6W5lBPeUEvisznV/HZD2IfZmF3Bns6cPC3Z0vCtSEOaAWTjYWN3jVsdu55jMxKlsdlhg=="], | |
| 698 | "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.63.0", "", { "os": "linux", "cpu": "arm" }, "sha512-I5r3twFf776UZg9dmRo2xbrKt00tTkORXEVe0ctg4vdTkQvJAjiCHxnbAU2HL1AiJ9cqADA76MAliuilsAWnvg=="], | |
| 681 | 699 | |
| 682 | "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+AZK8rOUr78y8WT6XkDb04IbMRqauNV+vgT6f8ZLOH8wnpQ9i7Nol0XLxAu+Cq7Sb+J9wC0j6Km5hG8rj47/yQ=="], | |
| 700 | "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.63.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-t7ltUkg6FFh4b564QyGir8xIj/QZbXu8FlcRkcyW9+ztr/mfRHlvUOFd95pJCXi9s/L5DrUeWWgpXRS+V+6igQ=="], | |
| 683 | 701 | |
| 684 | "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-urse2SnugwJRojUkGSSeH2LPMaje5Q50yQtvtL9HFckiyeqXzoFwOAZqD5TR29R2lq7UHidfFDM9EGcchcbb8A=="], | |
| 702 | "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.63.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Q5mmZy/XWjuYFUuQyYjOvZ5U/JkKEwnpir6hGxhh6HcdP0V/BKxLo8dqkfF/t7r7AguB17dfS/8+go5AQDRR6g=="], | |
| 685 | 703 | |
| 686 | "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-rkTZkBfJ4TYLjansjSzL6mgZOdN5IvUnSq3oNJSLwBcNvy3dlgQtpHPrRxrCEbbcp7oQ6If0tkNaqfOsphYZ9g=="], | |
| 704 | "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.63.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-uBGtuZ0TzLB4x5wVa82HGNvYqY8buwDhyCnCP0R0gkk9szqVsP0MeTtD5HX7EsEuFIt+aYmYxuxeVxs3nTSwtQ=="], | |
| 687 | 705 | |
| 688 | "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-uqL1kMH3u69/e1CH2EJhP3CP28jw2ExLsku4o8RVAZ7fySo9zOyI2fy9pVlTAp4voBLVgzndXi3SgtdyCTa2aA=="], | |
| 706 | "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.63.0", "", { "os": "linux", "cpu": "none" }, "sha512-h4s6FwxE+9MeA181o0dnDwHP32Y/bG8EiB/vrD6Ib+AMt6haigDc/0bUtI/sLmQDBMJnUfaCmtSSrEAqjtEVrA=="], | |
| 689 | 707 | |
| 690 | "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-j0CcMBOgV6KsRaBdsebIeiy7hCjEvq2KdEsiULf2LZqAq0v1M1lWjelhCV57LxsqaIGChXFuFJ0RiFrSRHPhSg=="], | |
| 708 | "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.63.0", "", { "os": "linux", "cpu": "none" }, "sha512-2EaNcCBR8Mcjl5ARtuN3BdEpVkX7KpjSjMGZ/mJMIeaXgTtdz5ytg2VwygMSStA/k0ixfvZFoZOfjDEcouV5vQ=="], | |
| 691 | 709 | |
| 692 | "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-7VDOiL8cDG3DQ/CY3yKjbV1c4YPvc4vH8qW09Vv+5ukq3l/Kcyr6XGCd5NvxUmxqDb2vjMpM+eW/4JrEEsUetA=="], | |
| 710 | "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.63.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4hlf/fd7TrYYl3QrWWD0GocqJefwMu3cHQhmi2FvEB/YOvFb5DZN3SMBaPi7B1TM5DeypkEtrVib674q1KKPg=="], | |
| 693 | 711 | |
| 694 | "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-JGRpX0M+ikD3WpwJ7vKcHKV6Kg0dT52BW2Eu2BupXotYeqGXBrbY+QPkAyKO6MNgKozyTNaRh3r7g+VWgyAQYQ=="], | |
| 712 | "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.63.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Vgq9rkRVcPcjbcH+ihYTfpeR7vCXfqpd+z5ItTGc0yYUV59L5ceHYN1iV4H9bKGV7Rn5hkVc7x3mSvHegduENA=="], | |
| 695 | 713 | |
| 696 | "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-dNaICPvtmuxFP/VbqdofrLqdS3bM/AKJN3LMJD52si44ea7Be1cBk6NpfIahaysG9Uo+L98QKddU9CD5L8UHnQ=="], | |
| 714 | "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.63.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3/Lkq/ncooA61rorrC+ZQed1Bc4VpGj+WnGsp58zmxKgvZ2vhreu+dcVyr3mX8NUpq7mfZ4gDDTou/yrF1Pd7A=="], | |
| 697 | 715 | |
| 698 | "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pF1vOtM+GuXmbklM1hV8WMsn6tCNPvkUzklj/Ej98JhlanbmA2RB1BILgOpwSuCTRTIYx2MXssmEyQQ90QF5aA=="], | |
| 716 | "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.63.0", "", { "os": "none", "cpu": "arm64" }, "sha512-0/EdD/6hDkx5Mfd769PTjvEM8mZ/6Dfukp1dBCL/2PjlIVGEtYdNZyok6ChqYPsT9JcFnlQnUeQzO0/1L/oC9w=="], | |
| 699 | 717 | |
| 700 | "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-bp8NQ4RE6fDIFLa4bdBiOA+TAvkNkg+rslR+AvvjlLTYXLy9/uKAYLQudaQouWihLD/hgkrXIKKzXi5IXOewwg=="], | |
| 718 | "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.63.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-wb0CUkN8ngwPiRQBjD1Cj0LsHeNvm+Xt6YBHDMtj2DVQVD6Oj8Ri7g6BD+KICf6LaBqZlmzOvy6nF9E/8yyGOg=="], | |
| 701 | 719 | |
| 702 | "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-PxT4OJDfMOQBzo3OlzFb9gkoSD+n8qSBxyVq2wQSZIHFQYGEqIRTo9M0ZStvZm5fdhMqaVYpOnJvH2hUMEDk/g=="], | |
| 720 | "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.63.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-BX5iq+ovdNlVYhSn5qPMUIT0uwAwt2lmEnCnzK+Gkhw4DovIvhGb96OFhV8yzQNUnQxn/xGkOR+X+BLrLDNm8w=="], | |
| 703 | 721 | |
| 704 | "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-PTRy6sIEPqy2x8PTP1baBNReN/BNEFmde0L+mYeHmjXE1Vlcc9+I5nsqENsB2yAm5wLkzPoTNCMY/7AnabT4/A=="], | |
| 722 | "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.63.0", "", { "os": "win32", "cpu": "x64" }, "sha512-QeN/WELOfsXMeYwxvfgQrl6CbVftYUCZsGXHjXQd5Trccm8+i4gmtxaOui4xbJQaiDlviF8F3yLSBloQUeFsfA=="], | |
| 705 | 723 | |
| 706 | "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ=="], | |
| 724 | "@oxlint/plugins": ["@oxlint/plugins@1.63.0", "", {}, "sha512-vZAzaUQkwgdN62RHgPFzfCsiBI6SDJMUdUlBGpJK0V++UHCMUk7UeJseygOhE/wOUSgV3ccE4ORkgab3C3MC6g=="], | |
| 707 | 725 | |
| 708 | 726 | "@pierre/diffs": ["@pierre/diffs@1.1.20", "", { "dependencies": { "@pierre/theme": "0.0.28", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-lLi+3sLCm3QDd5/aLO9pw+WbF6UzhrkWm2oTZ5WZJTGemOyUNRJ4DDhcEKmVusu4C4bXx9Nssh6fF+wQcapb5w=="], |
| 709 | 727 | |
| ... | ... | @@ -783,6 +801,8 @@ |
| 783 | 801 | |
| 784 | 802 | "@t3tools/marketing": ["@t3tools/marketing@workspace:apps/marketing"], |
| 785 | 803 | |
| 804 | "@t3tools/oxlint-plugin-t3code": ["@t3tools/oxlint-plugin-t3code@workspace:oxlint-plugin-t3code"], | |
| 805 | ||
| 786 | 806 | "@t3tools/scripts": ["@t3tools/scripts@workspace:scripts"], |
| 787 | 807 | |
| 788 | 808 | "@t3tools/shared": ["@t3tools/shared@workspace:packages/shared"], |
| ... | ... | @@ -1639,7 +1659,7 @@ |
| 1639 | 1659 | |
| 1640 | 1660 | "oxfmt": ["oxfmt@0.40.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.40.0", "@oxfmt/binding-android-arm64": "0.40.0", "@oxfmt/binding-darwin-arm64": "0.40.0", "@oxfmt/binding-darwin-x64": "0.40.0", "@oxfmt/binding-freebsd-x64": "0.40.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.40.0", "@oxfmt/binding-linux-arm-musleabihf": "0.40.0", "@oxfmt/binding-linux-arm64-gnu": "0.40.0", "@oxfmt/binding-linux-arm64-musl": "0.40.0", "@oxfmt/binding-linux-ppc64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-musl": "0.40.0", "@oxfmt/binding-linux-s390x-gnu": "0.40.0", "@oxfmt/binding-linux-x64-gnu": "0.40.0", "@oxfmt/binding-linux-x64-musl": "0.40.0", "@oxfmt/binding-openharmony-arm64": "0.40.0", "@oxfmt/binding-win32-arm64-msvc": "0.40.0", "@oxfmt/binding-win32-ia32-msvc": "0.40.0", "@oxfmt/binding-win32-x64-msvc": "0.40.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-g0C3I7xUj4b4DcagevM9kgH6+pUHytikxUcn3/VUkvzTNaaXBeyZqb7IBsHwojeXm4mTBEC/aBjBTMVUkZwWUQ=="], |
| 1641 | 1661 | |
| 1642 | "oxlint": ["oxlint@1.56.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.56.0", "@oxlint/binding-android-arm64": "1.56.0", "@oxlint/binding-darwin-arm64": "1.56.0", "@oxlint/binding-darwin-x64": "1.56.0", "@oxlint/binding-freebsd-x64": "1.56.0", "@oxlint/binding-linux-arm-gnueabihf": "1.56.0", "@oxlint/binding-linux-arm-musleabihf": "1.56.0", "@oxlint/binding-linux-arm64-gnu": "1.56.0", "@oxlint/binding-linux-arm64-musl": "1.56.0", "@oxlint/binding-linux-ppc64-gnu": "1.56.0", "@oxlint/binding-linux-riscv64-gnu": "1.56.0", "@oxlint/binding-linux-riscv64-musl": "1.56.0", "@oxlint/binding-linux-s390x-gnu": "1.56.0", "@oxlint/binding-linux-x64-gnu": "1.56.0", "@oxlint/binding-linux-x64-musl": "1.56.0", "@oxlint/binding-openharmony-arm64": "1.56.0", "@oxlint/binding-win32-arm64-msvc": "1.56.0", "@oxlint/binding-win32-ia32-msvc": "1.56.0", "@oxlint/binding-win32-x64-msvc": "1.56.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-Q+5Mj5PVaH/R6/fhMMFzw4dT+KPB+kQW4kaL8FOIq7tfhlnEVp6+3lcWqFruuTNlUo9srZUW3qH7Id4pskeR6g=="], | |
| 1662 | "oxlint": ["oxlint@1.63.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.63.0", "@oxlint/binding-android-arm64": "1.63.0", "@oxlint/binding-darwin-arm64": "1.63.0", "@oxlint/binding-darwin-x64": "1.63.0", "@oxlint/binding-freebsd-x64": "1.63.0", "@oxlint/binding-linux-arm-gnueabihf": "1.63.0", "@oxlint/binding-linux-arm-musleabihf": "1.63.0", "@oxlint/binding-linux-arm64-gnu": "1.63.0", "@oxlint/binding-linux-arm64-musl": "1.63.0", "@oxlint/binding-linux-ppc64-gnu": "1.63.0", "@oxlint/binding-linux-riscv64-gnu": "1.63.0", "@oxlint/binding-linux-riscv64-musl": "1.63.0", "@oxlint/binding-linux-s390x-gnu": "1.63.0", "@oxlint/binding-linux-x64-gnu": "1.63.0", "@oxlint/binding-linux-x64-musl": "1.63.0", "@oxlint/binding-openharmony-arm64": "1.63.0", "@oxlint/binding-win32-arm64-msvc": "1.63.0", "@oxlint/binding-win32-ia32-msvc": "1.63.0", "@oxlint/binding-win32-x64-msvc": "1.63.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-9TGXetdjgIHOJ9OiReomP7nnrMkV9HxC1xM2ramJSLQpzxjsAJtQwa4wqkJN2f/uCrqZuJseFuSlWDdvcruveg=="], | |
| 1643 | 1663 | |
| 1644 | 1664 | "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], |
| 1645 | 1665 |
oxlint-plugin-t3code/index.ts created+12| ... | ... | @@ -0,0 +1,12 @@ |
| 1 | import { definePlugin } from "@oxlint/plugins"; | |
| 2 | ||
| 3 | import noInlineSchemaCompile from "./rules/no-inline-schema-compile.ts"; | |
| 4 | ||
| 5 | export default definePlugin({ | |
| 6 | meta: { | |
| 7 | name: "t3code", | |
| 8 | }, | |
| 9 | rules: { | |
| 10 | "no-inline-schema-compile": noInlineSchemaCompile, | |
| 11 | }, | |
| 12 | }); |
oxlint-plugin-t3code/package.json created+21| ... | ... | @@ -0,0 +1,21 @@ |
| 1 | { | |
| 2 | "name": "@t3tools/oxlint-plugin-t3code", | |
| 3 | "private": true, | |
| 4 | "type": "module", | |
| 5 | "scripts": { | |
| 6 | "typecheck": "tsc --noEmit", | |
| 7 | "test": "vitest run" | |
| 8 | }, | |
| 9 | "dependencies": { | |
| 10 | "@effect/platform-node": "catalog:", | |
| 11 | "@oxlint/plugins": "^1.63.0", | |
| 12 | "effect": "catalog:" | |
| 13 | }, | |
| 14 | "devDependencies": { | |
| 15 | "@effect/language-service": "catalog:", | |
| 16 | "@effect/vitest": "catalog:", | |
| 17 | "@types/bun": "catalog:", | |
| 18 | "typescript": "catalog:", | |
| 19 | "vitest": "catalog:" | |
| 20 | } | |
| 21 | } |
oxlint-plugin-t3code/rules/no-inline-schema-compile.test.ts created+83| ... | ... | @@ -0,0 +1,83 @@ |
| 1 | import { assert, describe } from "@effect/vitest"; | |
| 2 | ||
| 3 | import { createOxlintRuleHarness } from "../test/utils.ts"; | |
| 4 | ||
| 5 | const rule = createOxlintRuleHarness("t3code/no-inline-schema-compile"); | |
| 6 | ||
| 7 | describe("t3code/no-inline-schema-compile", () => { | |
| 8 | rule.valid( | |
| 9 | "allows schema compilers hoisted to module scope", | |
| 10 | ` | |
| 11 | import { Schema } from "effect"; | |
| 12 | ||
| 13 | const User = Schema.Struct({ name: Schema.String }); | |
| 14 | const decodeUser = Schema.decodeUnknownEffect(User); | |
| 15 | ||
| 16 | export const parseUser = (input: unknown) => decodeUser(input); | |
| 17 | `, | |
| 18 | ); | |
| 19 | ||
| 20 | rule.valid( | |
| 21 | "allows factory helpers that return a precompiled decoder", | |
| 22 | ` | |
| 23 | import { Schema } from "effect"; | |
| 24 | ||
| 25 | export const makeParser = <A, I>(schema: Schema.Codec<A, I>) => { | |
| 26 | const decode = Schema.decodeUnknownEffect(schema); | |
| 27 | return (input: unknown) => decode(input); | |
| 28 | }; | |
| 29 | `, | |
| 30 | ); | |
| 31 | ||
| 32 | rule.valid( | |
| 33 | "allows schema construction helpers that use encode transformations", | |
| 34 | ` | |
| 35 | import { Schema } from "effect"; | |
| 36 | ||
| 37 | export const makePrettyJson = <S extends Schema.Top>(schema: S) => | |
| 38 | Schema.fromJsonString(schema).pipe( | |
| 39 | Schema.encode({ | |
| 40 | decode: Schema.String, | |
| 41 | encode: Schema.String, | |
| 42 | }), | |
| 43 | ); | |
| 44 | `, | |
| 45 | ); | |
| 46 | ||
| 47 | rule.valid( | |
| 48 | "allows dynamic schema parameters that cannot be hoisted to module scope", | |
| 49 | ` | |
| 50 | import { Schema } from "effect"; | |
| 51 | ||
| 52 | export const parseWith = <A, I>(schema: Schema.Codec<A, I>, input: unknown) => | |
| 53 | Schema.decodeUnknownEffect(schema)(input); | |
| 54 | `, | |
| 55 | ); | |
| 56 | ||
| 57 | rule.invalid( | |
| 58 | "reports schema compilers inside function bodies", | |
| 59 | ` | |
| 60 | import { Schema } from "effect"; | |
| 61 | ||
| 62 | const User = Schema.Struct({ name: Schema.String }); | |
| 63 | ||
| 64 | export const parseUser = (input: unknown) => Schema.decodeUnknownEffect(User)(input); | |
| 65 | `, | |
| 66 | (output) => { | |
| 67 | assert.match(output, /Hoist Schema\.decodeUnknownEffect/); | |
| 68 | }, | |
| 69 | ); | |
| 70 | ||
| 71 | rule.invalid( | |
| 72 | "reports inline schema literals as high confidence findings", | |
| 73 | ` | |
| 74 | import { Schema } from "effect"; | |
| 75 | ||
| 76 | export const parseUser = (input: unknown) => | |
| 77 | Schema.decodeUnknownEffect(Schema.Struct({ name: Schema.String }))(input); | |
| 78 | `, | |
| 79 | (output) => { | |
| 80 | assert.match(output, /inline schema literal and the compiled function/); | |
| 81 | }, | |
| 82 | ); | |
| 83 | }); |
oxlint-plugin-t3code/rules/no-inline-schema-compile.ts created+150| ... | ... | @@ -0,0 +1,150 @@ |
| 1 | import { defineRule } from "@oxlint/plugins"; | |
| 2 | import * as Option from "effect/Option"; | |
| 3 | import * as Schema from "effect/Schema"; | |
| 4 | ||
| 5 | import { getPropertyName, isIdentifier, unwrapExpression } from "../utils.ts"; | |
| 6 | ||
| 7 | // Effect Schema decoder/encoder APIs allocate compiled functions. Keep them | |
| 8 | // outside function bodies so hot paths do not rebuild compilers per call. | |
| 9 | const COMPILER_METHODS = new Set<keyof typeof Schema>([ | |
| 10 | "is", | |
| 11 | "asserts", | |
| 12 | "decodeEffect", | |
| 13 | "decodeExit", | |
| 14 | "decodeOption", | |
| 15 | "decodePromise", | |
| 16 | "decodeSync", | |
| 17 | "decodeUnknownExit", | |
| 18 | "decodeUnknownEffect", | |
| 19 | "decodeUnknownOption", | |
| 20 | "decodeUnknownPromise", | |
| 21 | "decodeUnknownSync", | |
| 22 | ||
| 23 | "encodeExit", | |
| 24 | "encodeEffect", | |
| 25 | "encodeOption", | |
| 26 | "encodePromise", | |
| 27 | "encodeSync", | |
| 28 | "encodeUnknownExit", | |
| 29 | "encodeUnknownEffect", | |
| 30 | "encodeUnknownOption", | |
| 31 | "encodeUnknownPromise", | |
| 32 | "encodeUnknownSync", | |
| 33 | ]); | |
| 34 | ||
| 35 | const getSchemaCompilerMethod = (callee: unknown): Option.Option<string> => { | |
| 36 | const expression = unwrapExpression(callee); | |
| 37 | if (Option.isNone(expression) || expression.value.type !== "MemberExpression") { | |
| 38 | return Option.none(); | |
| 39 | } | |
| 40 | ||
| 41 | const object = unwrapExpression(expression.value.object); | |
| 42 | if (!isIdentifier(object, "Schema")) return Option.none(); | |
| 43 | ||
| 44 | return Option.filter(getPropertyName(expression.value.property), (method) => | |
| 45 | COMPILER_METHODS.has(method as keyof typeof Schema), | |
| 46 | ); | |
| 47 | }; | |
| 48 | ||
| 49 | const isStaticSchemaReference = (node: unknown): boolean => { | |
| 50 | const expression = unwrapExpression(node); | |
| 51 | if (Option.isNone(expression)) return false; | |
| 52 | ||
| 53 | if (expression.value.type === "Identifier") { | |
| 54 | const [firstChar] = expression.value.name; | |
| 55 | return firstChar !== undefined && firstChar.toUpperCase() === firstChar; | |
| 56 | } | |
| 57 | ||
| 58 | return expression.value.type === "MemberExpression"; | |
| 59 | }; | |
| 60 | ||
| 61 | const isNestedStaticSchemaCall = (node: unknown): boolean => { | |
| 62 | const expression = unwrapExpression(node); | |
| 63 | if (Option.isNone(expression) || expression.value.type !== "CallExpression") return false; | |
| 64 | ||
| 65 | const callee = unwrapExpression(expression.value.callee); | |
| 66 | if (Option.isNone(callee) || callee.value.type !== "MemberExpression") return false; | |
| 67 | ||
| 68 | const object = unwrapExpression(callee.value.object); | |
| 69 | if (!isIdentifier(object, "Schema")) return false; | |
| 70 | ||
| 71 | const method = getPropertyName(callee.value.property); | |
| 72 | if (Option.isSome(method) && method.value === "fromJsonString") { | |
| 73 | const firstArg = expression.value.arguments[0]; | |
| 74 | return isStaticSchemaReference(firstArg) || isNestedStaticSchemaCall(firstArg); | |
| 75 | } | |
| 76 | ||
| 77 | return true; | |
| 78 | }; | |
| 79 | ||
| 80 | const isImmediatelyInvoked = (node: unknown): boolean => { | |
| 81 | const expression = unwrapExpression(node); | |
| 82 | if (Option.isNone(expression)) return false; | |
| 83 | ||
| 84 | const parent = | |
| 85 | "parent" in expression.value ? unwrapExpression(expression.value.parent) : Option.none(); | |
| 86 | return ( | |
| 87 | Option.isSome(parent) && | |
| 88 | parent.value.type === "CallExpression" && | |
| 89 | unwrapExpression(parent.value.callee).pipe( | |
| 90 | Option.exists((callee) => callee === expression.value), | |
| 91 | ) | |
| 92 | ); | |
| 93 | }; | |
| 94 | ||
| 95 | const messageHigh = (method: string) => | |
| 96 | `Hoist Schema.${method}(...) to module scope: both the inline schema literal and the compiled function are rebuilt on every call. Move the compiled function to a module-level const.`; | |
| 97 | ||
| 98 | const messageMedium = (method: string) => | |
| 99 | `Hoist Schema.${method}(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.`; | |
| 100 | ||
| 101 | export default defineRule({ | |
| 102 | meta: { | |
| 103 | type: "problem", | |
| 104 | docs: { | |
| 105 | description: | |
| 106 | "Disallow Schema decoder/encoder compiler calls inside function bodies; hoist them to module scope.", | |
| 107 | }, | |
| 108 | }, | |
| 109 | createOnce(context) { | |
| 110 | let functionDepth = 0; | |
| 111 | ||
| 112 | const resetFunctionDepth = () => { | |
| 113 | functionDepth = 0; | |
| 114 | }; | |
| 115 | ||
| 116 | const enterFunction = () => { | |
| 117 | functionDepth++; | |
| 118 | }; | |
| 119 | ||
| 120 | const exitFunction = () => { | |
| 121 | functionDepth--; | |
| 122 | }; | |
| 123 | ||
| 124 | return { | |
| 125 | before: resetFunctionDepth, | |
| 126 | FunctionDeclaration: enterFunction, | |
| 127 | "FunctionDeclaration:exit": exitFunction, | |
| 128 | FunctionExpression: enterFunction, | |
| 129 | "FunctionExpression:exit": exitFunction, | |
| 130 | ArrowFunctionExpression: enterFunction, | |
| 131 | "ArrowFunctionExpression:exit": exitFunction, | |
| 132 | CallExpression(node) { | |
| 133 | if (functionDepth === 0) return; | |
| 134 | ||
| 135 | const method = getSchemaCompilerMethod(node.callee); | |
| 136 | if (Option.isNone(method)) return; | |
| 137 | if (!isImmediatelyInvoked(node)) return; | |
| 138 | ||
| 139 | const firstArg = node.arguments[0]; | |
| 140 | const high = firstArg && isNestedStaticSchemaCall(firstArg); | |
| 141 | if (!high && !isStaticSchemaReference(firstArg)) return; | |
| 142 | ||
| 143 | context.report({ | |
| 144 | node: node.callee, | |
| 145 | message: high ? messageHigh(method.value) : messageMedium(method.value), | |
| 146 | }); | |
| 147 | }, | |
| 148 | }; | |
| 149 | }, | |
| 150 | }); |
oxlint-plugin-t3code/test/utils.ts created+152| ... | ... | @@ -0,0 +1,152 @@ |
| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; | |
| 2 | import { assert, it } from "@effect/vitest"; | |
| 3 | import * as Data from "effect/Data"; | |
| 4 | import * as Effect from "effect/Effect"; | |
| 5 | import * as FileSystem from "effect/FileSystem"; | |
| 6 | import * as Path from "effect/Path"; | |
| 7 | import * as PlatformError from "effect/PlatformError"; | |
| 8 | import * as Predicate from "effect/Predicate"; | |
| 9 | import * as Schema from "effect/Schema"; | |
| 10 | import * as Stream from "effect/Stream"; | |
| 11 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | |
| 12 | ||
| 13 | class OxlintFixtureFailure extends Data.TaggedError("OxlintFixtureFailure")<{ | |
| 14 | readonly exitCode: number; | |
| 15 | readonly stdout: string; | |
| 16 | readonly stderr: string; | |
| 17 | }> { | |
| 18 | static readonly is = (u: unknown): u is OxlintFixtureFailure => | |
| 19 | Predicate.isTagged(u, "OxlintFixtureFailure"); | |
| 20 | } | |
| 21 | ||
| 22 | class OxlintFixtureExpectedFailure extends Data.TaggedError("OxlintFixtureExpectedFailure")<{ | |
| 23 | readonly ruleName: string; | |
| 24 | }> { | |
| 25 | override get message() { | |
| 26 | return `Expected oxlint to report a failure for rule ${this.ruleName}, but it passed.`; | |
| 27 | } | |
| 28 | } | |
| 29 | ||
| 30 | const encodeOxlintConfig = Schema.encodeEffect(Schema.UnknownFromJsonString); | |
| 31 | ||
| 32 | interface RuleHarness { | |
| 33 | readonly run: ( | |
| 34 | source: string, | |
| 35 | ) => Effect.Effect< | |
| 36 | string, | |
| 37 | OxlintFixtureFailure | PlatformError.PlatformError | Schema.SchemaError, | |
| 38 | NodeServices.NodeServices | |
| 39 | >; | |
| 40 | readonly runAndExpectFailure: ( | |
| 41 | source: string, | |
| 42 | ) => Effect.Effect< | |
| 43 | string, | |
| 44 | OxlintFixtureExpectedFailure | PlatformError.PlatformError | Schema.SchemaError, | |
| 45 | NodeServices.NodeServices | |
| 46 | >; | |
| 47 | readonly valid: (name: string, source: string) => void; | |
| 48 | readonly invalid: (name: string, source: string, assertion?: (output: string) => void) => void; | |
| 49 | } | |
| 50 | ||
| 51 | const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> => | |
| 52 | stream.pipe( | |
| 53 | Stream.decodeText(), | |
| 54 | Stream.runFold( | |
| 55 | () => "", | |
| 56 | (acc, chunk) => acc + chunk, | |
| 57 | ), | |
| 58 | ); | |
| 59 | ||
| 60 | const spawnAndCollectOutput = Effect.fnUntraced(function* (command: ChildProcess.Command) { | |
| 61 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | |
| 62 | const child = yield* spawner.spawn(command); | |
| 63 | ||
| 64 | const [stdout, stderr, exitCode] = yield* Effect.all( | |
| 65 | [ | |
| 66 | collectStreamAsString(child.stdout), | |
| 67 | collectStreamAsString(child.stderr), | |
| 68 | child.exitCode.pipe(Effect.map(Number)), | |
| 69 | ], | |
| 70 | { concurrency: "unbounded" }, | |
| 71 | ); | |
| 72 | ||
| 73 | return { exitCode, stdout, stderr }; | |
| 74 | }, Effect.scoped); | |
| 75 | ||
| 76 | export const createOxlintRuleHarness = (ruleName: string): RuleHarness => { | |
| 77 | const [pluginName, shortRuleName] = ruleName.split("/"); | |
| 78 | const diagnosticRuleName = | |
| 79 | pluginName && shortRuleName ? `${pluginName}\\(${shortRuleName}\\)` : ruleName; | |
| 80 | const test = it.layer(NodeServices.layer); | |
| 81 | ||
| 82 | const run: RuleHarness["run"] = Effect.fnUntraced(function* (source: string) { | |
| 83 | const fs = yield* FileSystem.FileSystem; | |
| 84 | const path = yield* Path.Path; | |
| 85 | const fixtureDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-oxlint-" }); | |
| 86 | const configPath = path.join(fixtureDir, ".oxlintrc.json"); | |
| 87 | const sourcePath = path.join(fixtureDir, "fixture.ts"); | |
| 88 | const repoRoot = path.join(import.meta.dirname, "..", ".."); | |
| 89 | const oxlintBin = path.join(repoRoot, "node_modules", ".bin", "oxlint"); | |
| 90 | const pluginPath = path.join(repoRoot, "oxlint-plugin-t3code", "index.ts"); | |
| 91 | ||
| 92 | yield* fs.writeFileString( | |
| 93 | configPath, | |
| 94 | yield* encodeOxlintConfig({ | |
| 95 | jsPlugins: [{ name: "t3code", specifier: pluginPath }], | |
| 96 | rules: { [ruleName]: "error" }, | |
| 97 | }), | |
| 98 | ); | |
| 99 | yield* fs.writeFileString(sourcePath, source); | |
| 100 | ||
| 101 | const output = yield* spawnAndCollectOutput( | |
| 102 | ChildProcess.make(oxlintBin, ["--config", configPath, sourcePath], { cwd: repoRoot }), | |
| 103 | ); | |
| 104 | ||
| 105 | if (output.exitCode !== 0) { | |
| 106 | return yield* new OxlintFixtureFailure({ | |
| 107 | exitCode: output.exitCode, | |
| 108 | stdout: output.stdout, | |
| 109 | stderr: output.stderr, | |
| 110 | }); | |
| 111 | } | |
| 112 | ||
| 113 | return `${output.stdout}${output.stderr}`; | |
| 114 | }, Effect.scoped); | |
| 115 | ||
| 116 | const runAndExpectFailure: RuleHarness["runAndExpectFailure"] = (source) => | |
| 117 | run(source).pipe( | |
| 118 | Effect.matchEffect({ | |
| 119 | onFailure: (error) => | |
| 120 | OxlintFixtureFailure.is(error) | |
| 121 | ? Effect.succeed( | |
| 122 | `oxlint fixture failed with exit code ${error.exitCode}\n${error.stdout}\n${error.stderr}`, | |
| 123 | ) | |
| 124 | : Effect.fail(error), | |
| 125 | onSuccess: () => Effect.fail(new OxlintFixtureExpectedFailure({ ruleName })), | |
| 126 | }), | |
| 127 | ); | |
| 128 | ||
| 129 | return { | |
| 130 | run, | |
| 131 | runAndExpectFailure, | |
| 132 | valid(name, source) { | |
| 133 | test(name, (it) => { | |
| 134 | it.effect("passes", () => run(source)); | |
| 135 | }); | |
| 136 | }, | |
| 137 | invalid(name, source, assertion) { | |
| 138 | test(name, (it) => { | |
| 139 | it.effect("reports the rule diagnostic", () => | |
| 140 | runAndExpectFailure(source).pipe( | |
| 141 | Effect.tap((output) => | |
| 142 | Effect.sync(() => { | |
| 143 | assert.match(output, new RegExp(diagnosticRuleName)); | |
| 144 | assertion?.(output); | |
| 145 | }), | |
| 146 | ), | |
| 147 | ), | |
| 148 | ); | |
| 149 | }); | |
| 150 | }, | |
| 151 | }; | |
| 152 | }; |
oxlint-plugin-t3code/tsconfig.json created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | { | |
| 2 | "extends": "../tsconfig.base.json", | |
| 3 | "compilerOptions": { | |
| 4 | "composite": true, | |
| 5 | "types": ["bun", "node"], | |
| 6 | "lib": ["ESNext", "esnext.disposable"] | |
| 7 | }, | |
| 8 | "include": ["**/*.ts"] | |
| 9 | } |
oxlint-plugin-t3code/utils.ts created+58| ... | ... | @@ -0,0 +1,58 @@ |
| 1 | import type { ESTree } from "@oxlint/plugins"; | |
| 2 | import * as Option from "effect/Option"; | |
| 3 | ||
| 4 | type ExpressionWrapper = | |
| 5 | | ESTree.ChainExpression | |
| 6 | | ESTree.ParenthesizedExpression | |
| 7 | | ESTree.TSNonNullExpression | |
| 8 | | ESTree.TSAsExpression | |
| 9 | | ESTree.TSTypeAssertion; | |
| 10 | ||
| 11 | type AstNode = ESTree.Node; | |
| 12 | ||
| 13 | const asAstNode = (node: unknown): Option.Option<AstNode> => | |
| 14 | typeof node === "object" && node !== null && "type" in node && typeof node.type === "string" | |
| 15 | ? Option.some(node as AstNode) | |
| 16 | : Option.none(); | |
| 17 | ||
| 18 | const isExpressionWrapper = (node: AstNode): node is ExpressionWrapper => | |
| 19 | node.type === "ChainExpression" || | |
| 20 | node.type === "ParenthesizedExpression" || | |
| 21 | node.type === "TSNonNullExpression" || | |
| 22 | node.type === "TSAsExpression" || | |
| 23 | node.type === "TSTypeAssertion"; | |
| 24 | ||
| 25 | export function unwrapExpression(node: unknown): Option.Option<AstNode> { | |
| 26 | let current = asAstNode(node); | |
| 27 | ||
| 28 | while (Option.isSome(current) && isExpressionWrapper(current.value)) { | |
| 29 | current = asAstNode(current.value.expression); | |
| 30 | } | |
| 31 | ||
| 32 | return current; | |
| 33 | } | |
| 34 | ||
| 35 | export function getPropertyName(node: unknown): Option.Option<string> { | |
| 36 | return Option.flatMap(asAstNode(node), (expression) => { | |
| 37 | if (expression.type === "Identifier" && typeof expression.name === "string") { | |
| 38 | return Option.some(expression.name); | |
| 39 | } | |
| 40 | if (expression.type === "PrivateIdentifier" && typeof expression.name === "string") { | |
| 41 | return Option.some(expression.name); | |
| 42 | } | |
| 43 | if (expression.type === "Literal" && typeof expression.value === "string") { | |
| 44 | return Option.some(expression.value); | |
| 45 | } | |
| 46 | return Option.none(); | |
| 47 | }); | |
| 48 | } | |
| 49 | ||
| 50 | export function isIdentifier(node: Option.Option<AstNode>, name?: string): boolean { | |
| 51 | if (Option.isNone(node)) return false; | |
| 52 | const expression = node.value; | |
| 53 | return ( | |
| 54 | expression.type === "Identifier" && | |
| 55 | typeof expression.name === "string" && | |
| 56 | (name === undefined || expression.name === name) | |
| 57 | ); | |
| 58 | } |
package.json+3-1| ... | ... | @@ -4,6 +4,7 @@ |
| 4 | 4 | "workspaces": { |
| 5 | 5 | "packages": [ |
| 6 | 6 | "apps/*", |
| 7 | "oxlint-plugin-t3code", | |
| 7 | 8 | "packages/*", |
| 8 | 9 | "scripts" |
| 9 | 10 | ], |
| ... | ... | @@ -61,9 +62,10 @@ |
| 61 | 62 | }, |
| 62 | 63 | "devDependencies": { |
| 63 | 64 | "@effect/language-service": "catalog:", |
| 65 | "@oxlint/plugins": "^1.63.0", | |
| 64 | 66 | "@types/node": "catalog:", |
| 65 | 67 | "oxfmt": "^0.40.0", |
| 66 | "oxlint": "^1.55.0", | |
| 68 | "oxlint": "^1.63.0", | |
| 67 | 69 | "turbo": "^2.3.3", |
| 68 | 70 | "typescript": "catalog:", |
| 69 | 71 | "vitest": "catalog:" |
packages/contracts/src/orchestration.test.ts+2-1| ... | ... | @@ -38,6 +38,7 @@ const decodeThreadTurnStartRequestedPayload = Schema.decodeUnknownEffect( |
| 38 | 38 | const decodeOrchestrationLatestTurn = Schema.decodeUnknownEffect(OrchestrationLatestTurn); |
| 39 | 39 | const decodeOrchestrationProposedPlan = Schema.decodeUnknownEffect(OrchestrationProposedPlan); |
| 40 | 40 | const decodeOrchestrationSession = Schema.decodeUnknownEffect(OrchestrationSession); |
| 41 | const encodeThreadCreatedPayload = Schema.encodeEffect(ThreadCreatedPayload); | |
| 41 | 42 | |
| 42 | 43 | function getOptionValue( |
| 43 | 44 | options: ReadonlyArray<{ id: string; value: unknown }> | undefined, |
| ... | ... | @@ -485,7 +486,7 @@ it.effect( |
| 485 | 486 | updatedAt: "2026-01-01T00:00:00.000Z", |
| 486 | 487 | }); |
| 487 | 488 | |
| 488 | const encoded = yield* Schema.encodeEffect(ThreadCreatedPayload)(decoded); | |
| 489 | const encoded = yield* encodeThreadCreatedPayload(decoded); | |
| 489 | 490 | assert.deepStrictEqual(encoded.modelSelection.options, [{ id: "fastMode", value: true }]); |
| 490 | 491 | }), |
| 491 | 492 | ); |
packages/contracts/src/settings.ts+1| ... | ... | @@ -279,6 +279,7 @@ export const CursorSettings = makeProviderSettingsSchema( |
| 279 | 279 | }, |
| 280 | 280 | ); |
| 281 | 281 | export type CursorSettings = typeof CursorSettings.Type; |
| 282 | ||
| 282 | 283 | export const OpenCodeSettings = makeProviderSettingsSchema( |
| 283 | 284 | { |
| 284 | 285 | enabled: Schema.Boolean.pipe( |
packages/effect-acp/scripts/generate.ts+21-13| ... | ... | @@ -28,15 +28,24 @@ interface GeneratedPaths { |
| 28 | 28 | readonly metaOutputPath: string; |
| 29 | 29 | } |
| 30 | 30 | |
| 31 | const UpstreamJsonSchemaSchema = Schema.Struct({ | |
| 32 | $defs: Schema.Record(Schema.String, Schema.Json), | |
| 33 | }); | |
| 31 | 34 | const MetaJsonSchema = Schema.Struct({ |
| 32 | 35 | agentMethods: Schema.Record(Schema.String, Schema.String), |
| 33 | 36 | clientMethods: Schema.Record(Schema.String, Schema.String), |
| 34 | 37 | version: Schema.Union([Schema.Number, Schema.String]), |
| 35 | 38 | }); |
| 39 | const encodeAgentMethods = Schema.encodeEffect( | |
| 40 | Schema.fromJsonString(MetaJsonSchema.fields.agentMethods), | |
| 41 | ); | |
| 42 | const encodeClientMethods = Schema.encodeEffect( | |
| 43 | Schema.fromJsonString(MetaJsonSchema.fields.clientMethods), | |
| 44 | ); | |
| 45 | const encodeVersion = Schema.encodeEffect(Schema.fromJsonString(MetaJsonSchema.fields.version)); | |
| 36 | 46 | |
| 37 | const UpstreamJsonSchemaSchema = Schema.Struct({ | |
| 38 | $defs: Schema.Record(Schema.String, Schema.Json), | |
| 39 | }); | |
| 47 | const decodeUpstreamSchema = Schema.decodeEffect(Schema.fromJsonString(UpstreamJsonSchemaSchema)); | |
| 48 | const decodeMetaJson = Schema.decodeEffect(Schema.fromJsonString(MetaJsonSchema)); | |
| 40 | 49 | |
| 41 | 50 | const getGeneratedPaths = Effect.fn("getGeneratedPaths")(function* () { |
| 42 | 51 | const path = yield* Path.Path; |
| ... | ... | @@ -86,12 +95,9 @@ const downloadSchemas = Effect.fn("downloadSchemas")(function* (tag: string) { |
| 86 | 95 | ); |
| 87 | 96 | }); |
| 88 | 97 | |
| 89 | const readJsonFile = Effect.fn("readJsonFile")(function* < | |
| 90 | S extends Schema.Top & { readonly DecodingServices: never }, | |
| 91 | >(schema: S, filePath: string) { | |
| 98 | const readFileString = Effect.fn("readJsonFile")(function* (filePath: string) { | |
| 92 | 99 | const fs = yield* FileSystem.FileSystem; |
| 93 | const raw = yield* fs.readFileString(filePath); | |
| 94 | return yield* Schema.decodeEffect(Schema.fromJsonString(schema))(raw); | |
| 100 | return yield* fs.readFileString(filePath); | |
| 95 | 101 | }); |
| 96 | 102 | |
| 97 | 103 | const writeGeneratedFiles = Effect.fn("writeGeneratedFiles")(function* ( |
| ... | ... | @@ -201,8 +207,10 @@ const generateSchemas = Effect.fn("generateSchemas")(function* (skipDownload: bo |
| 201 | 207 | yield* downloadSchemas(CURRENT_SCHEMA_RELEASE); |
| 202 | 208 | } |
| 203 | 209 | |
| 204 | const upstreamSchema = yield* readJsonFile(UpstreamJsonSchemaSchema, upstreamSchemaPath); | |
| 205 | const upstreamMeta = yield* readJsonFile(MetaJsonSchema, upstreamMetaPath); | |
| 210 | const upstreamSchema = yield* readFileString(upstreamSchemaPath).pipe( | |
| 211 | Effect.flatMap(decodeUpstreamSchema), | |
| 212 | ); | |
| 213 | const upstreamMeta = yield* readFileString(upstreamMetaPath).pipe(Effect.flatMap(decodeMetaJson)); | |
| 206 | 214 | const normalizedDefinitions = Object.fromEntries( |
| 207 | 215 | Object.entries(upstreamSchema.$defs).map(([name, schema]) => [ |
| 208 | 216 | name, |
| ... | ... | @@ -245,11 +253,11 @@ const generateSchemas = Effect.fn("generateSchemas")(function* (skipDownload: bo |
| 245 | 253 | |
| 246 | 254 | const metaOutput = [ |
| 247 | 255 | ...prelude, |
| 248 | `export const AGENT_METHODS = ${yield* Schema.encodeEffect(Schema.fromJsonString(MetaJsonSchema.fields.agentMethods))(upstreamMeta.agentMethods)} as const;`, | |
| 256 | `export const AGENT_METHODS = ${yield* encodeAgentMethods(upstreamMeta.agentMethods)} as const;`, | |
| 249 | 257 | "", |
| 250 | `export const CLIENT_METHODS = ${yield* Schema.encodeEffect(Schema.fromJsonString(MetaJsonSchema.fields.clientMethods))(upstreamMeta.clientMethods)} as const;`, | |
| 258 | `export const CLIENT_METHODS = ${yield* encodeClientMethods(upstreamMeta.clientMethods)} as const;`, | |
| 251 | 259 | "", |
| 252 | `export const PROTOCOL_VERSION = ${yield* Schema.encodeEffect(Schema.fromJsonString(MetaJsonSchema.fields.version))(upstreamMeta.version)} as const;`, | |
| 260 | `export const PROTOCOL_VERSION = ${yield* encodeVersion(upstreamMeta.version)} as const;`, | |
| 253 | 261 | "", |
| 254 | 262 | ].join("\n"); |
| 255 | 263 |
packages/effect-acp/src/_internal/shared.ts+4-2| ... | ... | @@ -5,6 +5,8 @@ import { RpcClientError } from "effect/unstable/rpc"; |
| 5 | 5 | |
| 6 | 6 | import * as AcpSchema from "../_generated/schema.gen.ts"; |
| 7 | 7 | import * as AcpError from "../errors.ts"; |
| 8 | const isError = Schema.is(AcpSchema.Error); | |
| 9 | const isAcpRequestError = Schema.is(AcpError.AcpRequestError); | |
| 8 | 10 | |
| 9 | 11 | const formatSchemaIssue = SchemaIssue.makeFormatterDefault(); |
| 10 | 12 | |
| ... | ... | @@ -20,7 +22,7 @@ export const callRpc = <A>( |
| 20 | 22 | }), |
| 21 | 23 | ), |
| 22 | 24 | ), |
| 23 | Effect.catchIf(Schema.is(AcpSchema.Error), (error) => | |
| 25 | Effect.catchIf(isError, (error) => | |
| 24 | 26 | Effect.fail(AcpError.AcpRequestError.fromProtocolError(error)), |
| 25 | 27 | ), |
| 26 | 28 | ); |
| ... | ... | @@ -35,7 +37,7 @@ export const runHandler = Effect.fnUntraced(function* <A, B>( |
| 35 | 37 | } |
| 36 | 38 | return yield* handler(payload).pipe( |
| 37 | 39 | Effect.mapError((error) => |
| 38 | Schema.is(AcpError.AcpRequestError)(error) | |
| 40 | isAcpRequestError(error) | |
| 39 | 41 | ? error.toProtocolError() |
| 40 | 42 | : AcpError.AcpRequestError.internalError(error.message).toProtocolError(), |
| 41 | 43 | ), |
packages/effect-acp/src/agent.test.ts+6-6| ... | ... | @@ -34,6 +34,10 @@ const SessionCancelNotification = jsonRpcNotification( |
| 34 | 34 | const ExtPingNotification = jsonRpcNotification("x/ping", Schema.Struct({ count: Schema.Number })); |
| 35 | 35 | const ExtRequest = jsonRpcRequest("x/test", Schema.Struct({ hello: Schema.String })); |
| 36 | 36 | const ExtResponse = jsonRpcResponse(Schema.Struct({ ok: Schema.Boolean })); |
| 37 | const decodeRequestPermissionRequest = Schema.decodeEffect( | |
| 38 | Schema.fromJsonString(RequestPermissionRequest), | |
| 39 | ); | |
| 40 | const decodeInitializeResponse = Schema.decodeEffect(Schema.fromJsonString(InitializeResponse)); | |
| 37 | 41 | |
| 38 | 42 | it.effect("effect-acp agent handles core agent requests and outbound client requests", () => |
| 39 | 43 | Effect.gen(function* () { |
| ... | ... | @@ -83,9 +87,7 @@ it.effect("effect-acp agent handles core agent requests and outbound client requ |
| 83 | 87 | }) |
| 84 | 88 | .pipe(Effect.forkScoped); |
| 85 | 89 | |
| 86 | const permissionRequest = yield* Schema.decodeEffect( | |
| 87 | Schema.fromJsonString(RequestPermissionRequest), | |
| 88 | )(yield* Queue.take(output)); | |
| 90 | const permissionRequest = yield* decodeRequestPermissionRequest(yield* Queue.take(output)); | |
| 89 | 91 | assert.equal(permissionRequest.jsonrpc, "2.0"); |
| 90 | 92 | assert.equal(permissionRequest.method, "session/request_permission"); |
| 91 | 93 | assert.deepEqual(permissionRequest.params, { |
| ... | ... | @@ -136,9 +138,7 @@ it.effect("effect-acp agent handles core agent requests and outbound client requ |
| 136 | 138 | }), |
| 137 | 139 | ); |
| 138 | 140 | |
| 139 | const initResponse = yield* Schema.decodeEffect(Schema.fromJsonString(InitializeResponse))( | |
| 140 | yield* Queue.take(output), | |
| 141 | ); | |
| 141 | const initResponse = yield* decodeInitializeResponse(yield* Queue.take(output)); | |
| 142 | 142 | assert.deepEqual(initResponse, { |
| 143 | 143 | jsonrpc: "2.0", |
| 144 | 144 | id: 2, |
packages/effect-acp/src/protocol.test.ts+23-22| ... | ... | @@ -41,6 +41,13 @@ const RequestPermissionRequest = jsonRpcRequest( |
| 41 | 41 | const RequestPermissionResponse = jsonRpcResponse(AcpSchema.RequestPermissionResponse); |
| 42 | 42 | const ExtRequest = jsonRpcRequest("x/test", Schema.Struct({ hello: Schema.String })); |
| 43 | 43 | const ExtResponse = jsonRpcResponse(Schema.Struct({ ok: Schema.Boolean })); |
| 44 | const decodeSessionCancelNotification = Schema.decodeEffect( | |
| 45 | Schema.fromJsonString(SessionCancelNotification), | |
| 46 | ); | |
| 47 | const decodeExtRequest = Schema.decodeEffect(Schema.fromJsonString(ExtRequest)); | |
| 48 | const decodeRequestPermissionResponse = Schema.decodeEffect( | |
| 49 | Schema.fromJsonString(RequestPermissionResponse), | |
| 50 | ); | |
| 44 | 51 | const mockPeerPath = Effect.map(Effect.service(Path.Path), (path) => |
| 45 | 52 | path.join(import.meta.dirname, "../test/fixtures/acp-mock-peer.ts"), |
| 46 | 53 | ); |
| ... | ... | @@ -79,16 +86,13 @@ it.layer(NodeServices.layer)("effect-acp protocol", (it) => { |
| 79 | 86 | |
| 80 | 87 | yield* transport.notify("session/cancel", { sessionId: "session-1" }); |
| 81 | 88 | const outbound = yield* Queue.take(output); |
| 82 | assert.deepEqual( | |
| 83 | yield* Schema.decodeEffect(Schema.fromJsonString(SessionCancelNotification))(outbound), | |
| 84 | { | |
| 85 | jsonrpc: "2.0", | |
| 86 | method: "session/cancel", | |
| 87 | params: { | |
| 88 | sessionId: "session-1", | |
| 89 | }, | |
| 89 | assert.deepEqual(yield* decodeSessionCancelNotification(outbound), { | |
| 90 | jsonrpc: "2.0", | |
| 91 | method: "session/cancel", | |
| 92 | params: { | |
| 93 | sessionId: "session-1", | |
| 90 | 94 | }, |
| 91 | ); | |
| 95 | }); | |
| 92 | 96 | |
| 93 | 97 | yield* Queue.offer( |
| 94 | 98 | input, |
| ... | ... | @@ -200,7 +204,7 @@ it.layer(NodeServices.layer)("effect-acp protocol", (it) => { |
| 200 | 204 | .request("x/test", { hello: "world" }) |
| 201 | 205 | .pipe(Effect.forkScoped); |
| 202 | 206 | const outbound = yield* Queue.take(output); |
| 203 | assert.deepEqual(yield* Schema.decodeEffect(Schema.fromJsonString(ExtRequest))(outbound), { | |
| 207 | assert.deepEqual(yield* decodeExtRequest(outbound), { | |
| 204 | 208 | jsonrpc: "2.0", |
| 205 | 209 | id: 1, |
| 206 | 210 | method: "x/test", |
| ... | ... | @@ -288,19 +292,16 @@ it.layer(NodeServices.layer)("effect-acp protocol", (it) => { |
| 288 | 292 | }); |
| 289 | 293 | |
| 290 | 294 | const outbound = yield* Queue.take(output); |
| 291 | assert.deepEqual( | |
| 292 | yield* Schema.decodeEffect(Schema.fromJsonString(RequestPermissionResponse))(outbound), | |
| 293 | { | |
| 294 | jsonrpc: "2.0", | |
| 295 | id: 0, | |
| 296 | result: { | |
| 297 | outcome: { | |
| 298 | outcome: "selected", | |
| 299 | optionId: "allow", | |
| 300 | }, | |
| 295 | assert.deepEqual(yield* decodeRequestPermissionResponse(outbound), { | |
| 296 | jsonrpc: "2.0", | |
| 297 | id: 0, | |
| 298 | result: { | |
| 299 | outcome: { | |
| 300 | outcome: "selected", | |
| 301 | optionId: "allow", | |
| 301 | 302 | }, |
| 302 | 303 | }, |
| 303 | ); | |
| 304 | }); | |
| 304 | 305 | }), |
| 305 | 306 | ); |
| 306 | 307 | |
| ... | ... | @@ -321,7 +322,7 @@ it.layer(NodeServices.layer)("effect-acp protocol", (it) => { |
| 321 | 322 | .request("x/test", { hello: "world" }) |
| 322 | 323 | .pipe(Effect.forkScoped); |
| 323 | 324 | const outbound = yield* Queue.take(output); |
| 324 | assert.deepEqual(yield* Schema.decodeEffect(Schema.fromJsonString(ExtRequest))(outbound), { | |
| 325 | assert.deepEqual(yield* decodeExtRequest(outbound), { | |
| 325 | 326 | jsonrpc: "2.0", |
| 326 | 327 | id: 1, |
| 327 | 328 | method: "x/test", |
packages/effect-acp/src/protocol.ts+4-4| ... | ... | @@ -16,6 +16,8 @@ import * as RpcServer from "effect/unstable/rpc/RpcServer"; |
| 16 | 16 | import * as AcpSchema from "./_generated/schema.gen.ts"; |
| 17 | 17 | import { CLIENT_METHODS } from "./_generated/meta.gen.ts"; |
| 18 | 18 | import * as AcpError from "./errors.ts"; |
| 19 | const isAcpError = Schema.is(AcpError.AcpError); | |
| 20 | const isAcpRequestError = Schema.is(AcpError.AcpRequestError); | |
| 19 | 21 | |
| 20 | 22 | export interface AcpProtocolLogEvent { |
| 21 | 23 | readonly direction: "incoming" | "outgoing"; |
| ... | ... | @@ -408,7 +410,7 @@ export const makeAcpPatchedProtocol = Effect.fn("makeAcpPatchedProtocol")(functi |
| 408 | 410 | ), |
| 409 | 411 | Effect.matchEffect({ |
| 410 | 412 | onFailure: (error) => { |
| 411 | const normalized: AcpError.AcpError = Schema.is(AcpError.AcpError)(error) | |
| 413 | const normalized: AcpError.AcpError = isAcpError(error) | |
| 412 | 414 | ? error |
| 413 | 415 | : new AcpError.AcpTransportError({ |
| 414 | 416 | detail: error instanceof Error ? error.message : String(error), |
| ... | ... | @@ -521,9 +523,7 @@ function isProtocolError( |
| 521 | 523 | } |
| 522 | 524 | |
| 523 | 525 | function normalizeToRequestError(error: AcpError.AcpError): AcpError.AcpRequestError { |
| 524 | return Schema.is(AcpError.AcpRequestError)(error) | |
| 525 | ? error | |
| 526 | : AcpError.AcpRequestError.internalError(error.message); | |
| 526 | return isAcpRequestError(error) ? error : AcpError.AcpRequestError.internalError(error.message); | |
| 527 | 527 | } |
| 528 | 528 | |
| 529 | 529 | function toRpcClientError(error: AcpError.AcpError): RpcClientError.RpcClientError { |
packages/effect-codex-app-server/scripts/generate.ts+4-2| ... | ... | @@ -38,6 +38,8 @@ const JsonSchemaDocument = Schema.StructWithRest( |
| 38 | 38 | }), |
| 39 | 39 | [Schema.Record(Schema.String, Schema.Json)], |
| 40 | 40 | ); |
| 41 | const decodeGithubContentEntries = Schema.decodeEffect(Schema.fromJsonString(GithubContentEntries)); | |
| 42 | const decodeJsonSchemaDocument = Schema.decodeEffect(Schema.fromJsonString(JsonSchemaDocument)); | |
| 41 | 43 | |
| 42 | 44 | interface GeneratedPaths { |
| 43 | 45 | readonly generatedDir: string; |
| ... | ... | @@ -178,7 +180,7 @@ const fetchText = Effect.fn("fetchText")(function* (url: string) { |
| 178 | 180 | |
| 179 | 181 | const fetchDirectoryEntries = Effect.fn("fetchDirectoryEntries")(function* (path: string) { |
| 180 | 182 | const raw = yield* fetchText(`${GITHUB_API_BASE}/${path}?ref=${UPSTREAM_REF}`); |
| 181 | return yield* Schema.decodeEffect(Schema.fromJsonString(GithubContentEntries))(raw); | |
| 183 | return yield* decodeGithubContentEntries(raw); | |
| 182 | 184 | }); |
| 183 | 185 | |
| 184 | 186 | function collectSchemaEntries( |
| ... | ... | @@ -545,7 +547,7 @@ const generateFiles = Effect.fn("generateFiles")(function* () { |
| 545 | 547 | |
| 546 | 548 | for (const file of jsonSchemaFiles) { |
| 547 | 549 | const raw = yield* fetchText(file.downloadUrl); |
| 548 | const parsed = yield* Schema.decodeEffect(Schema.fromJsonString(JsonSchemaDocument))(raw); | |
| 550 | const parsed = yield* decodeJsonSchemaDocument(raw); | |
| 549 | 551 | const localDefinitionNames = new Map( |
| 550 | 552 | Object.keys(parsed.definitions ?? {}).map((definitionName) => [ |
| 551 | 553 | definitionName, |
packages/effect-codex-app-server/src/errors.ts+2-1| ... | ... | @@ -143,9 +143,10 @@ export const CodexAppServerError = Schema.Union([ |
| 143 | 143 | ]); |
| 144 | 144 | |
| 145 | 145 | export type CodexAppServerError = typeof CodexAppServerError.Type; |
| 146 | const isCodexAppServerRequestError = Schema.is(CodexAppServerRequestError); | |
| 146 | 147 | |
| 147 | 148 | export function normalizeToRequestError(error: CodexAppServerError): CodexAppServerRequestError { |
| 148 | return Schema.is(CodexAppServerRequestError)(error) | |
| 149 | return isCodexAppServerRequestError(error) | |
| 149 | 150 | ? error |
| 150 | 151 | : CodexAppServerRequestError.internalError(error.message); |
| 151 | 152 | } |
packages/effect-codex-app-server/src/protocol.test.ts+2-2| ... | ... | @@ -11,11 +11,11 @@ import { assert, it } from "@effect/vitest"; |
| 11 | 11 | import * as CodexError from "./errors.ts"; |
| 12 | 12 | import * as CodexProtocol from "./protocol.ts"; |
| 13 | 13 | import { makeInMemoryStdio } from "./_internal/stdio.ts"; |
| 14 | const encodeUnknownJsonString = Schema.encodeUnknownSync(Schema.UnknownFromJsonString); | |
| 14 | 15 | |
| 15 | 16 | const encoder = new TextEncoder(); |
| 16 | 17 | |
| 17 | const encodeJsonl = (value: unknown) => | |
| 18 | encoder.encode(`${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(value)}\n`); | |
| 18 | const encodeJsonl = (value: unknown) => encoder.encode(`${encodeUnknownJsonString(value)}\n`); | |
| 19 | 19 | |
| 20 | 20 | const decodeJson = Schema.decodeEffect(Schema.UnknownFromJsonString); |
| 21 | 21 |
packages/effect-codex-app-server/src/protocol.ts+6-3| ... | ... | @@ -10,6 +10,9 @@ import * as Stream from "effect/Stream"; |
| 10 | 10 | |
| 11 | 11 | import * as CodexError from "./errors.ts"; |
| 12 | 12 | import { JsonRpcId, JsonRpcResponseEnvelope } from "./_internal/shared.ts"; |
| 13 | const isJsonRpcId = Schema.is(JsonRpcId); | |
| 14 | const isJsonRpcResponseEnvelope = Schema.is(JsonRpcResponseEnvelope); | |
| 15 | const isCodexAppServerError = Schema.is(CodexError.CodexAppServerError); | |
| 13 | 16 | |
| 14 | 17 | export interface CodexAppServerProtocolLogEvent { |
| 15 | 18 | readonly direction: "incoming" | "outgoing"; |
| ... | ... | @@ -72,7 +75,7 @@ function isIncomingRequest(value: unknown): value is CodexAppServerIncomingReque |
| 72 | 75 | if (!isObject(value) || typeof value.method !== "string") { |
| 73 | 76 | return false; |
| 74 | 77 | } |
| 75 | return Schema.is(JsonRpcId)(value.id); | |
| 78 | return isJsonRpcId(value.id); | |
| 76 | 79 | } |
| 77 | 80 | |
| 78 | 81 | function isIncomingNotification(value: unknown): value is CodexAppServerIncomingNotification { |
| ... | ... | @@ -80,7 +83,7 @@ function isIncomingNotification(value: unknown): value is CodexAppServerIncoming |
| 80 | 83 | } |
| 81 | 84 | |
| 82 | 85 | function isIncomingResponse(value: unknown): value is typeof JsonRpcResponseEnvelope.Type { |
| 83 | return Schema.is(JsonRpcResponseEnvelope)(value); | |
| 86 | return isJsonRpcResponseEnvelope(value); | |
| 84 | 87 | } |
| 85 | 88 | |
| 86 | 89 | const encodeJsonString = Schema.encodeUnknownEffect(Schema.UnknownFromJsonString); |
| ... | ... | @@ -114,7 +117,7 @@ const decodeWireMessage = ( |
| 114 | 117 | ); |
| 115 | 118 | |
| 116 | 119 | const normalizeIncomingError = (error: unknown, detail: string): CodexError.CodexAppServerError => |
| 117 | Schema.is(CodexError.CodexAppServerError)(error) | |
| 120 | isCodexAppServerError(error) | |
| 118 | 121 | ? error |
| 119 | 122 | : new CodexError.CodexAppServerTransportError({ |
| 120 | 123 | detail, |
packages/shared/src/observability.test.ts+185-180| ... | ... | @@ -80,6 +80,8 @@ const makeTestLayer = (tracePath: string) => |
| 80 | 80 | Layer.succeed(References.MinimumLogLevel, "Info"), |
| 81 | 81 | ); |
| 82 | 82 | |
| 83 | const nodeServicesIt = it.layer(NodeServices.layer); | |
| 84 | ||
| 83 | 85 | describe("observability", () => { |
| 84 | 86 | it("normalizes circular arrays, maps, and sets without recursing forever", () => { |
| 85 | 87 | const array: Array<unknown> = ["alpha"]; |
| ... | ... | @@ -118,191 +120,194 @@ describe("observability", () => { |
| 118 | 120 | ); |
| 119 | 121 | }); |
| 120 | 122 | |
| 121 | it.effect("flushes buffered trace records on close", () => | |
| 122 | Effect.scoped( | |
| 123 | Effect.gen(function* () { | |
| 124 | const fileSystem = yield* FileSystem.FileSystem; | |
| 125 | const path = yield* Path.Path; | |
| 126 | const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" }); | |
| 127 | const tracePath = path.join(tempDir, "shared.trace.ndjson"); | |
| 128 | ||
| 129 | const sink = yield* makeTraceSink({ | |
| 130 | filePath: tracePath, | |
| 131 | maxBytes: 1024, | |
| 132 | maxFiles: 2, | |
| 133 | batchWindowMs: 10_000, | |
| 134 | }); | |
| 135 | ||
| 136 | sink.push(makeRecord("alpha")); | |
| 137 | sink.push(makeRecord("beta")); | |
| 138 | yield* sink.close(); | |
| 139 | ||
| 140 | const lines = yield* readTraceRecords(tracePath); | |
| 141 | ||
| 142 | assert.equal(lines.length, 2); | |
| 143 | assert.equal(lines[0]?.name, "alpha"); | |
| 144 | assert.equal(lines[1]?.name, "beta"); | |
| 145 | }).pipe(Effect.provide(NodeServices.layer)), | |
| 146 | ), | |
| 147 | ); | |
| 123 | nodeServicesIt("node services", (it) => { | |
| 124 | it.effect("flushes buffered trace records on close", () => | |
| 125 | Effect.scoped( | |
| 126 | Effect.gen(function* () { | |
| 127 | const fileSystem = yield* FileSystem.FileSystem; | |
| 128 | const path = yield* Path.Path; | |
| 129 | const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" }); | |
| 130 | const tracePath = path.join(tempDir, "shared.trace.ndjson"); | |
| 131 | ||
| 132 | const sink = yield* makeTraceSink({ | |
| 133 | filePath: tracePath, | |
| 134 | maxBytes: 1024, | |
| 135 | maxFiles: 2, | |
| 136 | batchWindowMs: 10_000, | |
| 137 | }); | |
| 138 | ||
| 139 | sink.push(makeRecord("alpha")); | |
| 140 | sink.push(makeRecord("beta")); | |
| 141 | yield* sink.close(); | |
| 142 | ||
| 143 | const lines = yield* readTraceRecords(tracePath); | |
| 144 | ||
| 145 | assert.equal(lines.length, 2); | |
| 146 | assert.equal(lines[0]?.name, "alpha"); | |
| 147 | assert.equal(lines[1]?.name, "beta"); | |
| 148 | }), | |
| 149 | ), | |
| 150 | ); | |
| 148 | 151 | |
| 149 | it.effect("rotates the trace file when the configured max size is exceeded", () => | |
| 150 | Effect.scoped( | |
| 151 | Effect.gen(function* () { | |
| 152 | const fileSystem = yield* FileSystem.FileSystem; | |
| 153 | const path = yield* Path.Path; | |
| 154 | const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" }); | |
| 155 | const tracePath = path.join(tempDir, "shared.trace.ndjson"); | |
| 156 | ||
| 157 | const sink = yield* makeTraceSink({ | |
| 158 | filePath: tracePath, | |
| 159 | maxBytes: 180, | |
| 160 | maxFiles: 2, | |
| 161 | batchWindowMs: 10_000, | |
| 162 | }); | |
| 163 | ||
| 164 | for (let index = 0; index < 8; index += 1) { | |
| 165 | sink.push(makeRecord("rotate", `${index}-${"x".repeat(48)}`)); | |
| 166 | yield* sink.flush; | |
| 167 | } | |
| 168 | yield* sink.close(); | |
| 169 | ||
| 170 | const matchingFiles = (yield* fileSystem.readDirectory(tempDir)) | |
| 171 | .filter( | |
| 172 | (entry) => entry === "shared.trace.ndjson" || entry.startsWith("shared.trace.ndjson."), | |
| 173 | ) | |
| 174 | .toSorted(); | |
| 175 | ||
| 176 | assert.equal( | |
| 177 | matchingFiles.some((entry) => entry === "shared.trace.ndjson.1"), | |
| 178 | true, | |
| 179 | ); | |
| 180 | assert.equal( | |
| 181 | matchingFiles.some((entry) => entry === "shared.trace.ndjson.3"), | |
| 182 | false, | |
| 183 | ); | |
| 184 | }).pipe(Effect.provide(NodeServices.layer)), | |
| 185 | ), | |
| 186 | ); | |
| 152 | it.effect("rotates the trace file when the configured max size is exceeded", () => | |
| 153 | Effect.scoped( | |
| 154 | Effect.gen(function* () { | |
| 155 | const fileSystem = yield* FileSystem.FileSystem; | |
| 156 | const path = yield* Path.Path; | |
| 157 | const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" }); | |
| 158 | const tracePath = path.join(tempDir, "shared.trace.ndjson"); | |
| 159 | ||
| 160 | const sink = yield* makeTraceSink({ | |
| 161 | filePath: tracePath, | |
| 162 | maxBytes: 180, | |
| 163 | maxFiles: 2, | |
| 164 | batchWindowMs: 10_000, | |
| 165 | }); | |
| 166 | ||
| 167 | for (let index = 0; index < 8; index += 1) { | |
| 168 | sink.push(makeRecord("rotate", `${index}-${"x".repeat(48)}`)); | |
| 169 | yield* sink.flush; | |
| 170 | } | |
| 171 | yield* sink.close(); | |
| 172 | ||
| 173 | const matchingFiles = (yield* fileSystem.readDirectory(tempDir)) | |
| 174 | .filter( | |
| 175 | (entry) => | |
| 176 | entry === "shared.trace.ndjson" || entry.startsWith("shared.trace.ndjson."), | |
| 177 | ) | |
| 178 | .toSorted(); | |
| 179 | ||
| 180 | assert.equal( | |
| 181 | matchingFiles.some((entry) => entry === "shared.trace.ndjson.1"), | |
| 182 | true, | |
| 183 | ); | |
| 184 | assert.equal( | |
| 185 | matchingFiles.some((entry) => entry === "shared.trace.ndjson.3"), | |
| 186 | false, | |
| 187 | ); | |
| 188 | }), | |
| 189 | ), | |
| 190 | ); | |
| 187 | 191 | |
| 188 | it.effect("drops only the invalid trace record when serialization fails", () => | |
| 189 | Effect.scoped( | |
| 190 | Effect.gen(function* () { | |
| 191 | const fileSystem = yield* FileSystem.FileSystem; | |
| 192 | const path = yield* Path.Path; | |
| 193 | const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" }); | |
| 194 | const tracePath = path.join(tempDir, "shared.trace.ndjson"); | |
| 195 | ||
| 196 | const sink = yield* makeTraceSink({ | |
| 197 | filePath: tracePath, | |
| 198 | maxBytes: 1024, | |
| 199 | maxFiles: 2, | |
| 200 | batchWindowMs: 10_000, | |
| 201 | }); | |
| 202 | ||
| 203 | const circular: Array<unknown> = []; | |
| 204 | circular.push(circular); | |
| 205 | ||
| 206 | sink.push(makeRecord("alpha")); | |
| 207 | sink.push({ | |
| 208 | ...makeRecord("invalid"), | |
| 209 | attributes: { | |
| 210 | circular, | |
| 211 | }, | |
| 212 | } as TraceRecord); | |
| 213 | sink.push(makeRecord("beta")); | |
| 214 | yield* sink.close(); | |
| 215 | ||
| 216 | const lines = yield* readTraceRecords(tracePath); | |
| 217 | ||
| 218 | assert.deepStrictEqual( | |
| 219 | lines.map((line) => line.name), | |
| 220 | ["alpha", "beta"], | |
| 221 | ); | |
| 222 | }).pipe(Effect.provide(NodeServices.layer)), | |
| 223 | ), | |
| 224 | ); | |
| 192 | it.effect("drops only the invalid trace record when serialization fails", () => | |
| 193 | Effect.scoped( | |
| 194 | Effect.gen(function* () { | |
| 195 | const fileSystem = yield* FileSystem.FileSystem; | |
| 196 | const path = yield* Path.Path; | |
| 197 | const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" }); | |
| 198 | const tracePath = path.join(tempDir, "shared.trace.ndjson"); | |
| 199 | ||
| 200 | const sink = yield* makeTraceSink({ | |
| 201 | filePath: tracePath, | |
| 202 | maxBytes: 1024, | |
| 203 | maxFiles: 2, | |
| 204 | batchWindowMs: 10_000, | |
| 205 | }); | |
| 206 | ||
| 207 | const circular: Array<unknown> = []; | |
| 208 | circular.push(circular); | |
| 209 | ||
| 210 | sink.push(makeRecord("alpha")); | |
| 211 | sink.push({ | |
| 212 | ...makeRecord("invalid"), | |
| 213 | attributes: { | |
| 214 | circular, | |
| 215 | }, | |
| 216 | } as TraceRecord); | |
| 217 | sink.push(makeRecord("beta")); | |
| 218 | yield* sink.close(); | |
| 219 | ||
| 220 | const lines = yield* readTraceRecords(tracePath); | |
| 221 | ||
| 222 | assert.deepStrictEqual( | |
| 223 | lines.map((line) => line.name), | |
| 224 | ["alpha", "beta"], | |
| 225 | ); | |
| 226 | }), | |
| 227 | ), | |
| 228 | ); | |
| 225 | 229 | |
| 226 | it.effect("writes nested spans to disk and captures log messages as span events", () => | |
| 227 | Effect.scoped( | |
| 228 | Effect.gen(function* () { | |
| 229 | const fileSystem = yield* FileSystem.FileSystem; | |
| 230 | const path = yield* Path.Path; | |
| 231 | const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-local-tracer-" }); | |
| 232 | const tracePath = path.join(tempDir, "shared.trace.ndjson"); | |
| 233 | ||
| 234 | yield* Effect.scoped( | |
| 235 | Effect.gen(function* () { | |
| 236 | const program = Effect.gen(function* () { | |
| 237 | yield* Effect.annotateCurrentSpan({ | |
| 238 | "demo.parent": true, | |
| 239 | }); | |
| 240 | yield* Effect.logInfo("parent event"); | |
| 241 | yield* Effect.gen(function* () { | |
| 230 | it.effect("writes nested spans to disk and captures log messages as span events", () => | |
| 231 | Effect.scoped( | |
| 232 | Effect.gen(function* () { | |
| 233 | const fileSystem = yield* FileSystem.FileSystem; | |
| 234 | const path = yield* Path.Path; | |
| 235 | const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-local-tracer-" }); | |
| 236 | const tracePath = path.join(tempDir, "shared.trace.ndjson"); | |
| 237 | ||
| 238 | yield* Effect.scoped( | |
| 239 | Effect.gen(function* () { | |
| 240 | const program = Effect.gen(function* () { | |
| 242 | 241 | yield* Effect.annotateCurrentSpan({ |
| 243 | "demo.child": true, | |
| 242 | "demo.parent": true, | |
| 244 | 243 | }); |
| 245 | yield* Effect.logInfo("child event"); | |
| 246 | }).pipe(Effect.withSpan("child-span")); | |
| 247 | }).pipe(Effect.withSpan("parent-span")); | |
| 248 | ||
| 249 | yield* program.pipe(Effect.provide(makeTestLayer(tracePath))); | |
| 250 | }), | |
| 251 | ); | |
| 252 | ||
| 253 | const records = yield* readTraceRecords(tracePath); | |
| 254 | assert.equal(records.length, 2); | |
| 255 | ||
| 256 | const parent = records.find((record) => record.name === "parent-span"); | |
| 257 | const child = records.find((record) => record.name === "child-span"); | |
| 258 | ||
| 259 | assert.notEqual(parent, undefined); | |
| 260 | assert.notEqual(child, undefined); | |
| 261 | if (!parent || !child) { | |
| 262 | return; | |
| 263 | } | |
| 264 | ||
| 265 | assert.equal(child.parentSpanId, parent.spanId); | |
| 266 | assert.equal(parent.attributes["demo.parent"], true); | |
| 267 | assert.equal(child.attributes["demo.child"], true); | |
| 268 | assert.equal( | |
| 269 | parent.events.some((event) => event.name === "parent event"), | |
| 270 | true, | |
| 271 | ); | |
| 272 | assert.equal( | |
| 273 | child.events.some((event) => event.name === "child event"), | |
| 274 | true, | |
| 275 | ); | |
| 276 | assert.equal( | |
| 277 | child.events.some((event) => event.attributes["effect.logLevel"] === "INFO"), | |
| 278 | true, | |
| 279 | ); | |
| 280 | }).pipe(Effect.provide(NodeServices.layer)), | |
| 281 | ), | |
| 282 | ); | |
| 244 | yield* Effect.logInfo("parent event"); | |
| 245 | yield* Effect.gen(function* () { | |
| 246 | yield* Effect.annotateCurrentSpan({ | |
| 247 | "demo.child": true, | |
| 248 | }); | |
| 249 | yield* Effect.logInfo("child event"); | |
| 250 | }).pipe(Effect.withSpan("child-span")); | |
| 251 | }).pipe(Effect.withSpan("parent-span")); | |
| 252 | ||
| 253 | yield* program.pipe(Effect.provide(makeTestLayer(tracePath))); | |
| 254 | }), | |
| 255 | ); | |
| 256 | ||
| 257 | const records = yield* readTraceRecords(tracePath); | |
| 258 | assert.equal(records.length, 2); | |
| 259 | ||
| 260 | const parent = records.find((record) => record.name === "parent-span"); | |
| 261 | const child = records.find((record) => record.name === "child-span"); | |
| 262 | ||
| 263 | assert.notEqual(parent, undefined); | |
| 264 | assert.notEqual(child, undefined); | |
| 265 | if (!parent || !child) { | |
| 266 | return; | |
| 267 | } | |
| 268 | ||
| 269 | assert.equal(child.parentSpanId, parent.spanId); | |
| 270 | assert.equal(parent.attributes["demo.parent"], true); | |
| 271 | assert.equal(child.attributes["demo.child"], true); | |
| 272 | assert.equal( | |
| 273 | parent.events.some((event) => event.name === "parent event"), | |
| 274 | true, | |
| 275 | ); | |
| 276 | assert.equal( | |
| 277 | child.events.some((event) => event.name === "child event"), | |
| 278 | true, | |
| 279 | ); | |
| 280 | assert.equal( | |
| 281 | child.events.some((event) => event.attributes["effect.logLevel"] === "INFO"), | |
| 282 | true, | |
| 283 | ); | |
| 284 | }), | |
| 285 | ), | |
| 286 | ); | |
| 283 | 287 | |
| 284 | it.effect("serializes interrupted spans with an interrupted exit status", () => | |
| 285 | Effect.scoped( | |
| 286 | Effect.gen(function* () { | |
| 287 | const fileSystem = yield* FileSystem.FileSystem; | |
| 288 | const path = yield* Path.Path; | |
| 289 | const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-local-tracer-" }); | |
| 290 | const tracePath = path.join(tempDir, "shared.trace.ndjson"); | |
| 291 | ||
| 292 | yield* Effect.scoped( | |
| 293 | Effect.exit( | |
| 294 | Effect.interrupt.pipe( | |
| 295 | Effect.withSpan("interrupt-span"), | |
| 296 | Effect.provide(makeTestLayer(tracePath)), | |
| 288 | it.effect("serializes interrupted spans with an interrupted exit status", () => | |
| 289 | Effect.scoped( | |
| 290 | Effect.gen(function* () { | |
| 291 | const fileSystem = yield* FileSystem.FileSystem; | |
| 292 | const path = yield* Path.Path; | |
| 293 | const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-local-tracer-" }); | |
| 294 | const tracePath = path.join(tempDir, "shared.trace.ndjson"); | |
| 295 | ||
| 296 | yield* Effect.scoped( | |
| 297 | Effect.exit( | |
| 298 | Effect.interrupt.pipe( | |
| 299 | Effect.withSpan("interrupt-span"), | |
| 300 | Effect.provide(makeTestLayer(tracePath)), | |
| 301 | ), | |
| 297 | 302 | ), |
| 298 | ), | |
| 299 | ); | |
| 300 | ||
| 301 | const records = yield* readTraceRecords(tracePath); | |
| 302 | assert.equal(records.length, 1); | |
| 303 | assert.equal(records[0]?.name, "interrupt-span"); | |
| 304 | assert.equal(records[0]?.exit?._tag, "Interrupted"); | |
| 305 | }).pipe(Effect.provide(NodeServices.layer)), | |
| 306 | ), | |
| 307 | ); | |
| 303 | ); | |
| 304 | ||
| 305 | const records = yield* readTraceRecords(tracePath); | |
| 306 | assert.equal(records.length, 1); | |
| 307 | assert.equal(records[0]?.name, "interrupt-span"); | |
| 308 | assert.equal(records[0]?.exit?._tag, "Interrupted"); | |
| 309 | }), | |
| 310 | ), | |
| 311 | ); | |
| 312 | }); | |
| 308 | 313 | }); |
packages/shared/src/serverSettings.ts+6-5| ... | ... | @@ -1,10 +1,12 @@ |
| 1 | 1 | import { ServerSettings, type ServerSettingsPatch } from "@t3tools/contracts"; |
| 2 | import * as Option from "effect/Option"; | |
| 2 | 3 | import * as Schema from "effect/Schema"; |
| 3 | 4 | import { deepMerge } from "./Struct.ts"; |
| 4 | 5 | import { fromLenientJson } from "./schemaJson.ts"; |
| 5 | 6 | import { createModelSelection } from "./model.ts"; |
| 6 | 7 | |
| 7 | 8 | const ServerSettingsJson = fromLenientJson(ServerSettings); |
| 9 | const decodeServerSettingsJson = Schema.decodeUnknownOption(ServerSettingsJson); | |
| 8 | 10 | |
| 9 | 11 | export interface PersistedServerObservabilitySettings { |
| 10 | 12 | readonly otlpTracesUrl: string | undefined; |
| ... | ... | @@ -33,12 +35,11 @@ export function extractPersistedServerObservabilitySettings(input: { |
| 33 | 35 | export function parsePersistedServerObservabilitySettings( |
| 34 | 36 | raw: string, |
| 35 | 37 | ): PersistedServerObservabilitySettings { |
| 36 | try { | |
| 37 | const decoded = Schema.decodeUnknownSync(ServerSettingsJson)(raw); | |
| 38 | return extractPersistedServerObservabilitySettings(decoded); | |
| 39 | } catch { | |
| 40 | return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined }; | |
| 38 | const decoded = decodeServerSettingsJson(raw); | |
| 39 | if (Option.isSome(decoded)) { | |
| 40 | return extractPersistedServerObservabilitySettings(decoded.value); | |
| 41 | 41 | } |
| 42 | return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined }; | |
| 42 | 43 | } |
| 43 | 44 | |
| 44 | 45 | function shouldReplaceTextGenerationModelSelection( |
scripts/release-smoke.ts+1| ... | ... | @@ -24,6 +24,7 @@ const workspaceFiles = [ |
| 24 | 24 | "apps/desktop/package.json", |
| 25 | 25 | "apps/web/package.json", |
| 26 | 26 | "apps/marketing/package.json", |
| 27 | "oxlint-plugin-t3code/package.json", | |
| 27 | 28 | "packages/client-runtime/package.json", |
| 28 | 29 | "packages/contracts/package.json", |
| 29 | 30 | "packages/shared/package.json", |
scripts/update-release-package-versions.ts+3-2| ... | ... | @@ -26,7 +26,7 @@ interface UpdateReleasePackageVersionsOptions { |
| 26 | 26 | const PackageJsonSchema = Schema.Record(Schema.String, Schema.Unknown); |
| 27 | 27 | const PackageJsonPrettyJson = fromJsonStringPretty(PackageJsonSchema); |
| 28 | 28 | const decodePackageJson = Schema.decodeUnknownEffect(PackageJsonPrettyJson); |
| 29 | const encodePackageJson = Schema.encodeSync(PackageJsonPrettyJson); | |
| 29 | const encodePackageJson = Schema.encodeEffect(PackageJsonPrettyJson); | |
| 30 | 30 | |
| 31 | 31 | export const updateReleasePackageVersions = Effect.fn("updateReleasePackageVersions")(function* ( |
| 32 | 32 | version: string, |
| ... | ... | @@ -44,7 +44,8 @@ export const updateReleasePackageVersions = Effect.fn("updateReleasePackageVersi |
| 44 | 44 | continue; |
| 45 | 45 | } |
| 46 | 46 | |
| 47 | yield* fs.writeFileString(filePath, `${encodePackageJson({ ...packageJson, version })}\n`); | |
| 47 | const packageJsonString = yield* encodePackageJson({ ...packageJson, version }); | |
| 48 | yield* fs.writeFileString(filePath, `${packageJsonString}\n`); | |
| 48 | 49 | changed = true; |
| 49 | 50 | } |
| 50 | 51 |