diff --git a/apps/server/src/vcs/VcsProjectConfig.test.ts b/apps/server/src/vcs/VcsProjectConfig.test.ts index aac4beb7e32..a54ef2541e1 100644 --- a/apps/server/src/vcs/VcsProjectConfig.test.ts +++ b/apps/server/src/vcs/VcsProjectConfig.test.ts @@ -41,8 +41,7 @@ describe("VcsProjectConfig", () => { yield* fileSystem.makeDirectory(nested, { recursive: true }); yield* fileSystem.writeFileString( path.join(configDir, "vcs.json"), - // @effect-diagnostics-next-line preferSchemaOverJson:off - JSON.stringify({ vcs: { kind: "jj" } }), + '{"vcs":{"kind":"jj"}}', ); const config = yield* VcsProjectConfig.VcsProjectConfig; @@ -53,6 +52,26 @@ describe("VcsProjectConfig", () => { ); }); + it.layer(TestLayer)("ignores malformed .t3code/vcs.json files", (it) => { + it.effect("falls back to auto", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const root = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-vcs-config-test-", + }); + const configDir = path.join(root, ".t3code"); + yield* fileSystem.makeDirectory(configDir, { recursive: true }); + yield* fileSystem.writeFileString(path.join(configDir, "vcs.json"), "{ not-json"); + + const config = yield* VcsProjectConfig.VcsProjectConfig; + const kind = yield* config.resolveKind({ cwd: root }); + + assert.equal(kind, "auto"); + }), + ); + }); + it.layer(TestLayer)("falls back to auto when no config exists", (it) => { it.effect("returns auto", () => Effect.gen(function* () { diff --git a/apps/server/src/vcs/VcsProjectConfig.ts b/apps/server/src/vcs/VcsProjectConfig.ts index 3e5ee2347ce..2a6d6513ddf 100644 --- a/apps/server/src/vcs/VcsProjectConfig.ts +++ b/apps/server/src/vcs/VcsProjectConfig.ts @@ -2,6 +2,7 @@ import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; @@ -15,16 +16,10 @@ const ProjectVcsConfig = Schema.Struct({ ), vcsKind: Schema.optional(VcsDriverKind), }); -const isProjectVcsConfig = Schema.is(ProjectVcsConfig); +const ProjectVcsConfigJson = Schema.fromJsonString(ProjectVcsConfig); +const decodeProjectVcsConfig = Schema.decodeUnknownOption(ProjectVcsConfigJson); -interface ProjectVcsConfigFile { - readonly vcs?: - | { - readonly kind?: VcsDriverKindType | undefined; - } - | undefined; - readonly vcsKind?: VcsDriverKindType | undefined; -} +type ProjectVcsConfigFile = Schema.Schema.Type; export interface VcsProjectConfigResolveInput { readonly cwd: string; @@ -45,13 +40,8 @@ function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto return config.vcs?.kind ?? config.vcsKind ?? "auto"; } -function parseConfig(raw: string): ProjectVcsConfigFile | null { - try { - const parsed = JSON.parse(raw) as unknown; - return isProjectVcsConfig(parsed) ? parsed : null; - } catch { - return null; - } +function parseConfig(raw: string): Option.Option { + return decodeProjectVcsConfig(raw); } export const make = Effect.fn("makeVcsProjectConfig")(function* () { @@ -63,12 +53,12 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () { while (true) { const candidate = path.join(current, ".t3code", "vcs.json"); if (yield* fileSystem.exists(candidate).pipe(Effect.orElseSucceed(() => false))) { - return candidate; + return Option.some(candidate); } const parent = path.dirname(current); if (parent === current) { - return null; + return Option.none(); } current = parent; } @@ -78,26 +68,27 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () { configPath: string, ) { const raw = yield* fileSystem.readFileString(configPath).pipe( + Effect.map(Option.some), Effect.catch((error) => Effect.logWarning("failed to read VCS project config", { configPath, error, - }).pipe(Effect.as(null)), + }).pipe(Effect.as(Option.none())), ), ); - if (raw === null) { + if (Option.isNone(raw)) { return "auto" as const; } - const parsed = parseConfig(raw); - if (parsed === null) { + const parsed = parseConfig(raw.value); + if (Option.isNone(parsed)) { yield* Effect.logWarning("invalid VCS project config", { configPath, }); return "auto" as const; } - return configuredKind(parsed); + return configuredKind(parsed.value); }); const resolveKind: VcsProjectConfigShape["resolveKind"] = Effect.fn( @@ -108,11 +99,11 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () { } const configPath = yield* findConfigPath(input.cwd); - if (configPath === null) { + if (Option.isNone(configPath)) { return "auto"; } - return yield* readConfiguredKind(configPath); + return yield* readConfiguredKind(configPath.value); }); return VcsProjectConfig.of({ diff --git a/apps/server/src/workspace/Layers/WorkspaceEntries.ts b/apps/server/src/workspace/Layers/WorkspaceEntries.ts index 0c0ab638207..235bd717bbe 100644 --- a/apps/server/src/workspace/Layers/WorkspaceEntries.ts +++ b/apps/server/src/workspace/Layers/WorkspaceEntries.ts @@ -29,7 +29,7 @@ import { } from "../Services/WorkspaceEntries.ts"; import { WorkspacePaths } from "../Services/WorkspacePaths.ts"; -const WORKSPACE_CACHE_TTL_MS = 15_000; +const WORKSPACE_CACHE_TTL = Duration.seconds(15); const WORKSPACE_CACHE_MAX_KEYS = 4; const WORKSPACE_INDEX_MAX_ENTRIES = 25_000; const WORKSPACE_SCAN_READDIR_CONCURRENCY = 32; @@ -402,8 +402,7 @@ export const makeWorkspaceEntries = Effect.gen(function* () { buildWorkspaceIndex, { capacity: WORKSPACE_CACHE_MAX_KEYS, - timeToLive: (exit) => - Exit.isSuccess(exit) ? Duration.millis(WORKSPACE_CACHE_TTL_MS) : Duration.zero, + timeToLive: (exit) => (Exit.isSuccess(exit) ? WORKSPACE_CACHE_TTL : Duration.zero), }, );