-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Expand file tree
/
Copy pathproviderStatusCache.ts
More file actions
105 lines (91 loc) · 3.58 KB
/
Copy pathproviderStatusCache.ts
File metadata and controls
105 lines (91 loc) · 3.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import * as nodePath from "node:path";
import { type ServerProvider, ServerProvider as ServerProviderSchema } from "@t3tools/contracts";
import { Cause, Effect, FileSystem, Schema } from "effect";
import { writeFileStringAtomically } from "../atomicWrite.ts";
export const PROVIDER_CACHE_IDS = [
"codex",
"claudeAgent",
"opencode",
"cursor",
] as const satisfies ReadonlyArray<ServerProvider["provider"]>;
const decodeProviderStatusCache = Schema.decodeUnknownEffect(
Schema.fromJsonString(ServerProviderSchema),
);
const providerOrderRank = (provider: ServerProvider["provider"]): number => {
const rank = PROVIDER_CACHE_IDS.indexOf(provider);
return rank === -1 ? Number.MAX_SAFE_INTEGER : rank;
};
const mergeProviderModels = (
fallbackModels: ReadonlyArray<ServerProvider["models"][number]>,
cachedModels: ReadonlyArray<ServerProvider["models"][number]>,
): ReadonlyArray<ServerProvider["models"][number]> => {
const fallbackSlugs = new Set(fallbackModels.map((model) => model.slug));
return [...fallbackModels, ...cachedModels.filter((model) => !fallbackSlugs.has(model.slug))];
};
export const orderProviderSnapshots = (
providers: ReadonlyArray<ServerProvider>,
): ReadonlyArray<ServerProvider> =>
[...providers].toSorted(
(left, right) => providerOrderRank(left.provider) - providerOrderRank(right.provider),
);
export const hydrateCachedProvider = (input: {
readonly cachedProvider: ServerProvider;
readonly fallbackProvider: ServerProvider;
}): ServerProvider => {
if (
!input.fallbackProvider.enabled ||
input.cachedProvider.enabled !== input.fallbackProvider.enabled
) {
return input.fallbackProvider;
}
const { message: _fallbackMessage, ...fallbackWithoutMessage } = input.fallbackProvider;
const hydratedProvider: ServerProvider = {
...fallbackWithoutMessage,
models: mergeProviderModels(input.fallbackProvider.models, input.cachedProvider.models),
installed: input.cachedProvider.installed,
version: input.cachedProvider.version,
status: input.cachedProvider.status,
auth: input.cachedProvider.auth,
checkedAt: input.cachedProvider.checkedAt,
slashCommands: input.cachedProvider.slashCommands,
skills: input.cachedProvider.skills,
};
return input.cachedProvider.message
? { ...hydratedProvider, message: input.cachedProvider.message }
: hydratedProvider;
};
export const resolveProviderStatusCachePath = (input: {
readonly cacheDir: string;
readonly provider: ServerProvider["provider"];
}) => nodePath.join(input.cacheDir, `${input.provider}.json`);
export const readProviderStatusCache = (filePath: string) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const exists = yield* fs.exists(filePath).pipe(Effect.orElseSucceed(() => false));
if (!exists) {
return undefined;
}
const raw = yield* fs.readFileString(filePath).pipe(Effect.orElseSucceed(() => ""));
const trimmed = raw.trim();
if (trimmed.length === 0) {
return undefined;
}
return yield* decodeProviderStatusCache(trimmed).pipe(
Effect.matchCauseEffect({
onFailure: (cause) =>
Effect.logWarning("failed to parse provider status cache, ignoring", {
path: filePath,
issues: Cause.pretty(cause),
}).pipe(Effect.as(undefined)),
onSuccess: Effect.succeed,
}),
);
});
export const writeProviderStatusCache = (input: {
readonly filePath: string;
readonly provider: ServerProvider;
}) =>
writeFileStringAtomically({
filePath: input.filePath,
contents: `${JSON.stringify(input.provider, null, 2)}\n`,
});