model-compare-opencode-plugin 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +186 -2
- package/dist/tui.js +741 -0
- package/package.json +60 -4
- package/src/config.ts +197 -0
- package/src/core/catalog.ts +189 -0
- package/src/core/hash.ts +12 -0
- package/src/core/pins.ts +158 -0
- package/src/core/rank.ts +132 -0
- package/src/core/types.ts +50 -0
- package/src/index.ts +478 -0
- package/src/opencode/aliases.ts +58 -0
- package/src/opencode/commands.ts +251 -0
- package/src/opencode/guard.ts +44 -0
- package/src/opencode/refresh.ts +188 -0
- package/src/opencode/resolution.ts +69 -0
- package/src/opencode/storage.ts +97 -0
- package/src/opencode/usebest.ts +121 -0
- package/src/rpc.ts +78 -0
- package/src/sources/index.ts +115 -0
- package/src/sources/local.ts +175 -0
- package/src/sources/remote.ts +155 -0
- package/src/tui.tsx +772 -0
- package/vendor/UPSTREAM.json +6 -0
- package/vendor/model_compare.py +1950 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// src/sources/remote.ts
|
|
2
|
+
// The remote catalog source (design §5, §8, §9): fetch the published catalog
|
|
3
|
+
// document, validate it, and persist it as a versioned record so the last
|
|
4
|
+
// accepted document survives restarts (§9's offline path). A record younger
|
|
5
|
+
// than the TTL is served without a network round-trip; any fetch or validation
|
|
6
|
+
// failure falls back to the cached record at any age, and fetchRemote only
|
|
7
|
+
// rejects when there is no usable record at all.
|
|
8
|
+
|
|
9
|
+
import { validateCatalog } from "../core/catalog"
|
|
10
|
+
import type { Sourced } from "../core/types"
|
|
11
|
+
import {
|
|
12
|
+
readCachedDoc,
|
|
13
|
+
remoteIdentity,
|
|
14
|
+
writeCachedDoc,
|
|
15
|
+
type Storage,
|
|
16
|
+
} from "../opencode/storage"
|
|
17
|
+
|
|
18
|
+
// The cache is revalidated after six hours (design §8).
|
|
19
|
+
const DEFAULT_TTL_MS = 6 * 60 * 60 * 1000
|
|
20
|
+
|
|
21
|
+
// A fetch that has not answered by then is aborted (design §4: "HTTPS fetch
|
|
22
|
+
// with timeout"). Without it a dead socket would hold the refresh tick's
|
|
23
|
+
// non-overlap guard forever.
|
|
24
|
+
const DEFAULT_FETCH_TIMEOUT_MS = 30_000
|
|
25
|
+
|
|
26
|
+
// The body size cap (hostile-input hardening): `catalogURL` is user-pointable
|
|
27
|
+
// and the body is buffered whole for JSON.parse, so a hostile server must not
|
|
28
|
+
// be able to exhaust memory. 16 MiB is far above any sane catalog (the
|
|
29
|
+
// recorded fixture is well under 1 MiB).
|
|
30
|
+
const MAX_BODY_BYTES = 16 * 1024 * 1024 // 16 MiB
|
|
31
|
+
|
|
32
|
+
// Body-read failures share one taxonomy, the invalid-JSON reason style.
|
|
33
|
+
function bodyTooLargeReason(): string {
|
|
34
|
+
return `invalid JSON in response (response body exceeds the ${MAX_BODY_BYTES / (1024 * 1024)} MiB cap)`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Read the body as text, enforcing MAX_BODY_BYTES while reading: the stream
|
|
38
|
+
// is cancelled the moment the cap is crossed, so an endless or oversized body
|
|
39
|
+
// is never buffered further (a lying or absent Content-Length cannot bypass
|
|
40
|
+
// the cap).
|
|
41
|
+
async function readCappedText(
|
|
42
|
+
response: Response,
|
|
43
|
+
maxBytes: number,
|
|
44
|
+
): Promise<{ ok: true; text: string } | { ok: false }> {
|
|
45
|
+
const body = response.body
|
|
46
|
+
if (body === null) return { ok: true, text: "" }
|
|
47
|
+
const reader = body.getReader()
|
|
48
|
+
const chunks: Uint8Array[] = []
|
|
49
|
+
let total = 0
|
|
50
|
+
try {
|
|
51
|
+
for (;;) {
|
|
52
|
+
const { done, value } = await reader.read()
|
|
53
|
+
if (done) break
|
|
54
|
+
if (value === undefined) continue
|
|
55
|
+
total += value.byteLength
|
|
56
|
+
if (total > maxBytes) return { ok: false }
|
|
57
|
+
chunks.push(value)
|
|
58
|
+
}
|
|
59
|
+
} finally {
|
|
60
|
+
// On the overflow path reading stops here — free the connection instead
|
|
61
|
+
// of leaving the stream (and the socket behind it) half-drained.
|
|
62
|
+
await reader.cancel().catch(() => {})
|
|
63
|
+
}
|
|
64
|
+
const bytes = new Uint8Array(total)
|
|
65
|
+
let offset = 0
|
|
66
|
+
for (const chunk of chunks) {
|
|
67
|
+
bytes.set(chunk, offset)
|
|
68
|
+
offset += chunk.byteLength
|
|
69
|
+
}
|
|
70
|
+
return { ok: true, text: new TextDecoder().decode(bytes) }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Injectable seams for tests: the network call, the plugin's clock, and the
|
|
74
|
+
// TTL (design §8). All default to their production behavior.
|
|
75
|
+
export type RemoteDeps = {
|
|
76
|
+
fetch?: typeof fetch
|
|
77
|
+
now?: () => number
|
|
78
|
+
ttlMs?: number
|
|
79
|
+
timeoutMs?: number
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// One fetch-and-validate attempt as a result object (the fetch itself, the
|
|
83
|
+
// HTTP status, the body parse, and the document each get their own reason).
|
|
84
|
+
// Network errors surface with their own message; non-OK statuses name the
|
|
85
|
+
// status code.
|
|
86
|
+
async function fetchAndValidate(
|
|
87
|
+
url: string,
|
|
88
|
+
doFetch: typeof fetch,
|
|
89
|
+
now: () => number,
|
|
90
|
+
timeoutMs: number,
|
|
91
|
+
): Promise<{ ok: true; sourced: Sourced } | { ok: false; reason: string }> {
|
|
92
|
+
let response: Response
|
|
93
|
+
try {
|
|
94
|
+
response = await doFetch(url, { signal: AbortSignal.timeout(timeoutMs) })
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) }
|
|
97
|
+
}
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
return { ok: false, reason: `HTTP status ${response.status}` }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Content-Length, when honest and present, fails before a single body byte
|
|
103
|
+
// is read; readCappedText enforces the same cap for the rest.
|
|
104
|
+
const declared = response.headers?.get("content-length")
|
|
105
|
+
if (declared !== null && declared !== undefined && /^\d+$/.test(declared) && Number(declared) > MAX_BODY_BYTES) {
|
|
106
|
+
await response.body?.cancel().catch(() => {})
|
|
107
|
+
return { ok: false, reason: bodyTooLargeReason() }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let raw: unknown
|
|
111
|
+
try {
|
|
112
|
+
const read = await readCappedText(response, MAX_BODY_BYTES)
|
|
113
|
+
if (!read.ok) return { ok: false, reason: bodyTooLargeReason() }
|
|
114
|
+
raw = JSON.parse(read.text)
|
|
115
|
+
} catch (error) {
|
|
116
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
117
|
+
return { ok: false, reason: `invalid JSON in response (${message})` }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const validated = validateCatalog(raw)
|
|
121
|
+
if (!validated.ok) {
|
|
122
|
+
return { ok: false, reason: `invalid catalog document: ${validated.reason}` }
|
|
123
|
+
}
|
|
124
|
+
return { ok: true, sourced: { doc: validated.catalog, acquiredAt: now() } }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Fetch the remote catalog with a persisted six-hour cache (design §5, §8):
|
|
128
|
+
// a fresh record is returned without fetching; otherwise the document is
|
|
129
|
+
// fetched, validated, persisted in the document cache of this URL, and returned with
|
|
130
|
+
// fromCache: false. A fetch or validation failure returns the cached record
|
|
131
|
+
// whatever its age; with no usable record the reason is rejected.
|
|
132
|
+
export async function fetchRemote(
|
|
133
|
+
url: string,
|
|
134
|
+
storage: Storage,
|
|
135
|
+
deps: RemoteDeps = {},
|
|
136
|
+
): Promise<Sourced & { fromCache: boolean }> {
|
|
137
|
+
const doFetch = deps.fetch ?? globalThis.fetch
|
|
138
|
+
const now = deps.now ?? Date.now
|
|
139
|
+
const ttlMs = deps.ttlMs ?? DEFAULT_TTL_MS
|
|
140
|
+
|
|
141
|
+
const identity = remoteIdentity(url)
|
|
142
|
+
const cached = await readCachedDoc(storage, identity)
|
|
143
|
+
if (cached !== null && now() - cached.acquiredAt < ttlMs) {
|
|
144
|
+
return { ...cached, fromCache: true }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const attempt = await fetchAndValidate(url, doFetch, now, deps.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS)
|
|
148
|
+
if (attempt.ok) {
|
|
149
|
+
const { doc, acquiredAt } = attempt.sourced
|
|
150
|
+
await writeCachedDoc(storage, identity, { doc, acquiredAt })
|
|
151
|
+
return { doc, acquiredAt, fromCache: false }
|
|
152
|
+
}
|
|
153
|
+
if (cached !== null) return { ...cached, fromCache: true }
|
|
154
|
+
throw new Error(`remote source failed: ${attempt.reason}`)
|
|
155
|
+
}
|