@polderlabs/bizar 4.9.0 → 5.0.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.
Files changed (183) hide show
  1. package/bizar-dash/dist/assets/icons-Bo0iH9EC.js +701 -0
  2. package/bizar-dash/dist/assets/icons-Bo0iH9EC.js.map +1 -0
  3. package/bizar-dash/dist/assets/{index-DU61awG3.js → index-DmpSFPJY.js} +1 -1
  4. package/bizar-dash/dist/assets/{index-DU61awG3.js.map → index-DmpSFPJY.js.map} +1 -1
  5. package/bizar-dash/dist/assets/main-C1cpttnv.js +19 -0
  6. package/bizar-dash/dist/assets/main-C1cpttnv.js.map +1 -0
  7. package/bizar-dash/dist/assets/main-DTkNlLrw.css +1 -0
  8. package/bizar-dash/dist/assets/markdown-DIquRulQ.js +29 -0
  9. package/bizar-dash/dist/assets/markdown-DIquRulQ.js.map +1 -0
  10. package/bizar-dash/dist/assets/mobile-BtxQJftK.js +2 -0
  11. package/bizar-dash/dist/assets/mobile-BtxQJftK.js.map +1 -0
  12. package/bizar-dash/dist/assets/mobile-DD-FZrTC.js +1 -0
  13. package/bizar-dash/dist/assets/{mobile-CL5uUQEC.js.map → mobile-DD-FZrTC.js.map} +1 -1
  14. package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js +40 -0
  15. package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js.map +1 -0
  16. package/bizar-dash/dist/index.html +6 -3
  17. package/bizar-dash/dist/mobile.html +5 -2
  18. package/bizar-dash/node_modules/.package-lock.json +6 -0
  19. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  20. package/bizar-dash/package-lock.json +6 -0
  21. package/bizar-dash/skills/eval/SKILL.md +237 -0
  22. package/bizar-dash/src/server/api.mjs +35 -0
  23. package/bizar-dash/src/server/auth.mjs +155 -1
  24. package/bizar-dash/src/server/diagnostics-store.mjs +452 -2
  25. package/bizar-dash/src/server/eval-store.mjs +226 -0
  26. package/bizar-dash/src/server/eval.mjs +347 -0
  27. package/bizar-dash/src/server/memory-store.mjs +46 -0
  28. package/bizar-dash/src/server/ocr.mjs +55 -0
  29. package/bizar-dash/src/server/plugins/registry.mjs +363 -0
  30. package/bizar-dash/src/server/plugins/sandbox.mjs +655 -0
  31. package/bizar-dash/src/server/plugins/store.mjs +659 -0
  32. package/bizar-dash/src/server/providers-store.mjs +11 -4
  33. package/bizar-dash/src/server/routes/_shared.mjs +2 -2
  34. package/bizar-dash/src/server/routes/clipboard.mjs +173 -0
  35. package/bizar-dash/src/server/routes/doctor.mjs +71 -0
  36. package/bizar-dash/src/server/routes/env-vars.mjs +67 -1
  37. package/bizar-dash/src/server/routes/eval.mjs +147 -0
  38. package/bizar-dash/src/server/routes/memory.mjs +3 -0
  39. package/bizar-dash/src/server/routes/ocr.mjs +182 -0
  40. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +61 -42
  41. package/bizar-dash/src/server/routes/plugins.mjs +220 -0
  42. package/bizar-dash/src/server/routes/schedules.mjs +55 -0
  43. package/bizar-dash/src/server/routes/users.mjs +84 -0
  44. package/bizar-dash/src/server/routes/voice.mjs +131 -0
  45. package/bizar-dash/src/server/routes/workspaces.mjs +204 -0
  46. package/bizar-dash/src/server/serve-info.mjs +172 -0
  47. package/bizar-dash/src/server/voice-store.mjs +202 -0
  48. package/bizar-dash/src/server/voice-transcribe.mjs +72 -0
  49. package/bizar-dash/src/server/workspaces.mjs +626 -0
  50. package/bizar-dash/src/web/App.tsx +48 -11
  51. package/bizar-dash/src/web/components/AutosaveField.tsx +50 -0
  52. package/bizar-dash/src/web/components/DoctorPanel.tsx +160 -0
  53. package/bizar-dash/src/web/components/EnvVarManager.tsx +245 -62
  54. package/bizar-dash/src/web/components/InviteDialog.tsx +205 -0
  55. package/bizar-dash/src/web/components/ScheduleTemplateCard.tsx +71 -0
  56. package/bizar-dash/src/web/components/ScreenshotCapture.tsx +138 -0
  57. package/bizar-dash/src/web/components/ScreenshotOCR.tsx +42 -0
  58. package/bizar-dash/src/web/components/SettingsNav.tsx +101 -0
  59. package/bizar-dash/src/web/components/Sidebar.tsx +25 -1
  60. package/bizar-dash/src/web/components/StatusBadge.tsx +33 -2
  61. package/bizar-dash/src/web/components/Topbar.tsx +11 -0
  62. package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +182 -0
  63. package/bizar-dash/src/web/components/VoiceRecorder.tsx +104 -0
  64. package/bizar-dash/src/web/components/WorkspaceSelector.tsx +158 -0
  65. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +64 -0
  66. package/bizar-dash/src/web/components/chat/Composer.tsx +1 -1
  67. package/bizar-dash/src/web/components/chat/useChat.ts +118 -1
  68. package/bizar-dash/src/web/hooks/useAutosave.ts +107 -0
  69. package/bizar-dash/src/web/lib/types.ts +134 -0
  70. package/bizar-dash/src/web/styles/chat.css +8 -5
  71. package/bizar-dash/src/web/styles/main.css +77 -2
  72. package/bizar-dash/src/web/styles/memory.css +82 -0
  73. package/bizar-dash/src/web/styles/settings.css +265 -0
  74. package/bizar-dash/src/web/views/Chat.tsx +15 -1
  75. package/bizar-dash/src/web/views/Doctor.tsx +317 -0
  76. package/bizar-dash/src/web/views/Memory.tsx +17 -2
  77. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +1 -0
  78. package/bizar-dash/src/web/views/Schedules.tsx +94 -12
  79. package/bizar-dash/src/web/views/Settings.tsx +78 -45
  80. package/bizar-dash/src/web/views/Workspace.tsx +294 -0
  81. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +114 -133
  82. package/bizar-dash/src/web/views/memory/FromScreenshotPanel.tsx +23 -0
  83. package/bizar-dash/src/web/views/memory/VaultFromClipboardPanel.tsx +101 -0
  84. package/bizar-dash/src/web/views/settings/AgentSection.tsx +23 -12
  85. package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +5 -6
  86. package/bizar-dash/src/web/views/settings/GeneralSection.tsx +38 -15
  87. package/bizar-dash/src/web/views/settings/MemorySection.tsx +92 -8
  88. package/bizar-dash/src/web/views/settings/WorkspacesSection.tsx +189 -0
  89. package/bizar-dash/tests/autosave.test.tsx +276 -0
  90. package/bizar-dash/tests/bundle-analysis.test.mjs +5 -2
  91. package/bizar-dash/tests/chat-composer.test.tsx +140 -0
  92. package/bizar-dash/tests/clipboard.test.mjs +147 -0
  93. package/bizar-dash/tests/components/doctor-panel.test.tsx +105 -0
  94. package/bizar-dash/tests/components/screenshot-ocr.test.tsx +75 -0
  95. package/bizar-dash/tests/components/workspace-selector.test.tsx +73 -0
  96. package/bizar-dash/tests/deploy-templates.test.mjs +100 -0
  97. package/bizar-dash/tests/diagnostics-store.test.mjs +206 -0
  98. package/bizar-dash/tests/env-vars-extended.test.mjs +190 -0
  99. package/bizar-dash/tests/eval/fixtures.test.mjs +141 -0
  100. package/bizar-dash/tests/eval/report.test.mjs +284 -0
  101. package/bizar-dash/tests/eval/runner.test.mjs +471 -0
  102. package/bizar-dash/tests/layout-spacing.test.mjs +101 -0
  103. package/bizar-dash/tests/memory-default-vault.test.mjs +98 -0
  104. package/bizar-dash/tests/minimax-bar.test.tsx +113 -0
  105. package/bizar-dash/tests/minimax-models.test.mjs +123 -0
  106. package/bizar-dash/tests/ocr.test.mjs +87 -0
  107. package/bizar-dash/tests/opencode-sessions-detail.test.mjs +158 -0
  108. package/bizar-dash/tests/plugins-registry.test.mjs +387 -0
  109. package/bizar-dash/tests/plugins-sandbox.test.mjs +374 -0
  110. package/bizar-dash/tests/plugins-store.test.mjs +455 -0
  111. package/bizar-dash/tests/routes-doctor.test.mjs +159 -0
  112. package/bizar-dash/tests/schedules-templates.test.mjs +144 -0
  113. package/bizar-dash/tests/settings-layout.test.tsx +129 -0
  114. package/bizar-dash/tests/settings-mode-wiring.test.tsx +151 -0
  115. package/bizar-dash/tests/settings-nav.test.tsx +126 -0
  116. package/bizar-dash/tests/users.test.mjs +108 -0
  117. package/bizar-dash/tests/voice-recorder.test.tsx +95 -0
  118. package/bizar-dash/tests/voice-store.test.mjs +148 -0
  119. package/bizar-dash/tests/voice-transcribe.test.mjs +87 -0
  120. package/bizar-dash/tests/workspaces.test.mjs +527 -0
  121. package/cli/bin.mjs +72 -2
  122. package/cli/commands/clip.mjs +146 -0
  123. package/cli/commands/deploy/cloudflare.mjs +250 -0
  124. package/cli/commands/deploy/docker.mjs +221 -0
  125. package/cli/commands/deploy/fly.mjs +161 -0
  126. package/cli/commands/deploy/vercel.mjs +225 -0
  127. package/cli/commands/deploy.mjs +240 -0
  128. package/cli/commands/eval.mjs +378 -0
  129. package/cli/commands/marketplace.mjs +64 -0
  130. package/cli/commands/minimax.mjs +5 -2
  131. package/cli/commands/ocr.mjs +165 -0
  132. package/cli/commands/plugin.mjs +358 -0
  133. package/cli/commands/voice.mjs +211 -0
  134. package/cli/commands/workspace.mjs +247 -0
  135. package/cli/tests/minimax-cli.test.mjs +79 -0
  136. package/config/agents/frigg.md +1 -1
  137. package/config/agents/heimdall.md +1 -1
  138. package/config/agents/mimir.md +1 -1
  139. package/config/agents/quick.md +1 -1
  140. package/config/agents/semble-search.md +1 -1
  141. package/config/agents/vor.md +1 -1
  142. package/config/opencode.json +28 -21
  143. package/config/opencode.json.template +8 -1
  144. package/package.json +12 -8
  145. package/plugins/bizar/index.ts +77 -0
  146. package/plugins/bizar/src/compaction.d.mts +48 -0
  147. package/plugins/bizar/src/compaction.mjs +192 -0
  148. package/plugins/bizar/tests/compaction.test.ts +264 -0
  149. package/templates/deploy/cloudflare/README.md +32 -0
  150. package/templates/deploy/cloudflare/functions-index.template.js +15 -0
  151. package/templates/deploy/cloudflare/wrangler.toml.template +9 -0
  152. package/templates/deploy/docker/.env.template +16 -0
  153. package/templates/deploy/docker/README.md +58 -0
  154. package/templates/deploy/docker/docker-compose.template.yml +23 -0
  155. package/templates/deploy/fly/README.md +35 -0
  156. package/templates/deploy/fly/fly.toml.template +28 -0
  157. package/templates/deploy/vercel/README.md +29 -0
  158. package/templates/deploy/vercel/api-index.template.js +18 -0
  159. package/templates/deploy/vercel/vercel.json.template +16 -0
  160. package/templates/eval-fixtures/README.md +58 -0
  161. package/templates/eval-fixtures/code-search-basic.json +28 -0
  162. package/templates/eval-fixtures/latency-bounds.json +16 -0
  163. package/templates/eval-fixtures/regression-suite.json +79 -0
  164. package/templates/eval-fixtures/response-format.json +30 -0
  165. package/templates/eval-fixtures/tool-call-correctness.json +24 -0
  166. package/templates/plugin-template/README.md +121 -0
  167. package/templates/plugin-template/index.js +66 -0
  168. package/templates/plugin-template/plugin.json +42 -0
  169. package/templates/plugin-template/tests/plugin.test.js +83 -0
  170. package/templates/schedules/daily-backup.json +12 -0
  171. package/templates/schedules/daily-cleanup.json +12 -0
  172. package/templates/schedules/hourly-health-check.json +12 -0
  173. package/templates/schedules/webhook-on-push.json +13 -0
  174. package/templates/schedules/weekly-digest.json +13 -0
  175. package/bizar-dash/dist/assets/main-DaC1Lc6q.js +0 -366
  176. package/bizar-dash/dist/assets/main-DaC1Lc6q.js.map +0 -1
  177. package/bizar-dash/dist/assets/main-DfmIfOUS.css +0 -1
  178. package/bizar-dash/dist/assets/mobile-CL5uUQEC.js +0 -1
  179. package/bizar-dash/dist/assets/mobile-D5WTWvuh.js +0 -338
  180. package/bizar-dash/dist/assets/mobile-D5WTWvuh.js.map +0 -1
  181. package/bizar-dash/src/web/views/settings/BackupSection.tsx +0 -16
  182. package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +0 -16
  183. package/bizar-dash/src/web/views/settings/SkillsSection.tsx +0 -16
@@ -0,0 +1,363 @@
1
+ /**
2
+ * src/server/plugins/registry.mjs
3
+ *
4
+ * v5.0.0 — Plugin marketplace registry client.
5
+ *
6
+ * The marketplace registry is a single JSON document served over HTTP
7
+ * (default: raw.githubusercontent.com) listing every installable plugin.
8
+ * We fetch + parse + validate the shape, then cache the parsed value
9
+ * for an hour so a dashboard boot doesn't hammer GitHub.
10
+ *
11
+ * Shape (see README for the full schema):
12
+ * {
13
+ * "version": 1,
14
+ * "updatedAt": "2026-...",
15
+ * "plugins": [
16
+ * {
17
+ * "id": "vercel-deploy",
18
+ * "name": "Vercel Deploy",
19
+ * "version": "1.0.0",
20
+ * "description": "...",
21
+ * "author": "...",
22
+ * "category": "deploy",
23
+ * "tags": ["vercel", "deploy"],
24
+ * "homepage": "https://...",
25
+ * "tarball": "https://.../vercel-deploy-1.0.0.tar.gz",
26
+ * "checksum": "sha256:abcdef...",
27
+ * "permissions": ["net", "fs:read"],
28
+ * "minBizarVersion": "4.9.0"
29
+ * }
30
+ * ]
31
+ * }
32
+ */
33
+ import { createHash } from 'node:crypto';
34
+ import { createReadStream } from 'node:fs';
35
+
36
+ /**
37
+ * Default registry URL. Override with `BIZAR_REGISTRY_URL` env var.
38
+ *
39
+ * Raw GitHub is intentional: the registry is small, static, and
40
+ * versioned by commit (we point to `main/registry.json`). For private
41
+ * deployments, point this at an internal CDN or S3 bucket.
42
+ */
43
+ const DEFAULT_REGISTRY_URL =
44
+ 'https://raw.githubusercontent.com/DrB0rk/bizar-plugins/main/registry.json';
45
+
46
+ /** How long a cached registry is considered fresh. */
47
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
48
+
49
+ /**
50
+ * In-memory cache shared across all callers in this process. Tests
51
+ * can call `__resetCache()` to force a re-fetch.
52
+ */
53
+ let _cache = null; // { fetchedAt: number, url: string, data: RegistryShape }
54
+
55
+ /**
56
+ * Resolve the registry URL: explicit override > env > default.
57
+ *
58
+ * @param {string} [override]
59
+ * @returns {string}
60
+ */
61
+ export function getRegistryUrl(override) {
62
+ if (typeof override === 'string' && override.trim()) return override.trim();
63
+ if (process.env.BIZAR_REGISTRY_URL && process.env.BIZAR_REGISTRY_URL.trim()) {
64
+ return process.env.BIZAR_REGISTRY_URL.trim();
65
+ }
66
+ return DEFAULT_REGISTRY_URL;
67
+ }
68
+
69
+ /**
70
+ * Validate the parsed registry JSON shape. Throws a structured Error
71
+ * (`.code = 'invalid_registry'`) on any required-field violation. We
72
+ * are deliberately lenient on optional fields — missing `tags`,
73
+ * `homepage`, etc. are fine.
74
+ *
75
+ * @param {unknown} data
76
+ * @returns {RegistryShape}
77
+ */
78
+ export function validateRegistry(data) {
79
+ if (!data || typeof data !== 'object') {
80
+ const err = new Error('registry root must be an object');
81
+ err.code = 'invalid_registry';
82
+ throw err;
83
+ }
84
+ const root = /** @type {any} */ (data);
85
+ if (root.version !== 1) {
86
+ const err = new Error(`registry.version must be 1 (got ${root.version})`);
87
+ err.code = 'invalid_registry';
88
+ throw err;
89
+ }
90
+ if (typeof root.updatedAt !== 'string') {
91
+ const err = new Error('registry.updatedAt must be a string');
92
+ err.code = 'invalid_registry';
93
+ throw err;
94
+ }
95
+ if (!Array.isArray(root.plugins)) {
96
+ const err = new Error('registry.plugins must be an array');
97
+ err.code = 'invalid_registry';
98
+ throw err;
99
+ }
100
+ /** @type {string[]} */
101
+ const seenIds = new Set();
102
+ for (let i = 0; i < root.plugins.length; i++) {
103
+ const p = root.plugins[i];
104
+ if (!p || typeof p !== 'object') {
105
+ const err = new Error(`registry.plugins[${i}] must be an object`);
106
+ err.code = 'invalid_registry';
107
+ throw err;
108
+ }
109
+ if (typeof p.id !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(p.id)) {
110
+ const err = new Error(
111
+ `registry.plugins[${i}].id must be a kebab-case string (got ${JSON.stringify(p.id)})`,
112
+ );
113
+ err.code = 'invalid_registry';
114
+ throw err;
115
+ }
116
+ if (seenIds.has(p.id)) {
117
+ const err = new Error(`registry.plugins[${i}].id "${p.id}" is duplicated`);
118
+ err.code = 'invalid_registry';
119
+ throw err;
120
+ }
121
+ seenIds.add(p.id);
122
+ for (const required of ['name', 'version', 'tarball', 'checksum']) {
123
+ if (typeof p[required] !== 'string' || !p[required]) {
124
+ const err = new Error(
125
+ `registry.plugins[${i}].${required} must be a non-empty string`,
126
+ );
127
+ err.code = 'invalid_registry';
128
+ throw err;
129
+ }
130
+ }
131
+ if (typeof p.tarball !== 'string' || !/^https?:\/\//.test(p.tarball)) {
132
+ const err = new Error(
133
+ `registry.plugins[${i}].tarball must be an http(s) URL (got ${JSON.stringify(p.tarball)})`,
134
+ );
135
+ err.code = 'invalid_registry';
136
+ throw err;
137
+ }
138
+ if (typeof p.checksum !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(p.checksum)) {
139
+ const err = new Error(
140
+ `registry.plugins[${i}].checksum must be sha256:<64-hex> (got ${JSON.stringify(p.checksum)})`,
141
+ );
142
+ err.code = 'invalid_registry';
143
+ throw err;
144
+ }
145
+ if (!Array.isArray(p.permissions)) {
146
+ const err = new Error(
147
+ `registry.plugins[${i}].permissions must be an array`,
148
+ );
149
+ err.code = 'invalid_registry';
150
+ throw err;
151
+ }
152
+ }
153
+ return /** @type {RegistryShape} */ (root);
154
+ }
155
+
156
+ /**
157
+ * Fetch the registry, returning the validated parsed shape.
158
+ *
159
+ * Cache: in-memory for 1 hour per URL. A `force: true` option bypasses
160
+ * the cache (used by the `update` CLI subcommand).
161
+ *
162
+ * @param {object} [opts]
163
+ * @param {string} [opts.url] override the URL (default: env/default)
164
+ * @param {boolean} [opts.force] skip the cache
165
+ * @param {typeof globalThis.fetch} [opts.fetch] fetch override (tests)
166
+ * @returns {Promise<RegistryShape>}
167
+ */
168
+ export async function fetchRegistry({ url, force = false, fetch: fetchImpl } = {}) {
169
+ const resolvedUrl = getRegistryUrl(url);
170
+ const now = Date.now();
171
+ if (
172
+ !force &&
173
+ _cache &&
174
+ _cache.url === resolvedUrl &&
175
+ now - _cache.fetchedAt < CACHE_TTL_MS
176
+ ) {
177
+ return _cache.data;
178
+ }
179
+ const fetchFn = fetchImpl || globalThis.fetch;
180
+ if (typeof fetchFn !== 'function') {
181
+ throw new Error('fetch is not available in this runtime');
182
+ }
183
+ let res;
184
+ try {
185
+ res = await fetchFn(resolvedUrl, {
186
+ headers: { 'User-Agent': 'bizar-registry-client/5.0' },
187
+ });
188
+ } catch (err) {
189
+ const wrap = new Error(
190
+ `registry fetch failed for ${resolvedUrl}: ${err.message}`,
191
+ );
192
+ wrap.code = 'registry_unreachable';
193
+ wrap.cause = err;
194
+ throw wrap;
195
+ }
196
+ if (!res.ok) {
197
+ const err = new Error(
198
+ `registry fetch returned ${res.status} ${res.statusText} from ${resolvedUrl}`,
199
+ );
200
+ err.code = 'registry_unreachable';
201
+ err.status = res.status;
202
+ throw err;
203
+ }
204
+ let json;
205
+ try {
206
+ json = await res.json();
207
+ } catch (err) {
208
+ const wrap = new Error(
209
+ `registry at ${resolvedUrl} is not valid JSON: ${err.message}`,
210
+ );
211
+ wrap.code = 'invalid_registry';
212
+ wrap.cause = err;
213
+ throw wrap;
214
+ }
215
+ const validated = validateRegistry(json);
216
+ _cache = { fetchedAt: now, url: resolvedUrl, data: validated };
217
+ return validated;
218
+ }
219
+
220
+ /**
221
+ * Reset the in-memory cache. Tests use this to force a re-fetch after
222
+ * stubbing globalThis.fetch. Production code shouldn't need it.
223
+ */
224
+ export function __resetCache() {
225
+ _cache = null;
226
+ }
227
+
228
+ /**
229
+ * Normalize a string for fuzzy search comparison. Lowercases, strips
230
+ * punctuation, collapses whitespace.
231
+ *
232
+ * @param {string} s
233
+ */
234
+ function normalize(s) {
235
+ return String(s || '')
236
+ .toLowerCase()
237
+ .replace(/[^a-z0-9]+/g, ' ')
238
+ .trim();
239
+ }
240
+
241
+ /**
242
+ * Does the plugin match the query? Match is by substring on name +
243
+ * description + id, after normalization. Empty query matches all.
244
+ *
245
+ * @param {RegistryPlugin} plugin
246
+ * @param {string} query
247
+ */
248
+ function matchesQuery(plugin, query) {
249
+ if (!query) return true;
250
+ const n = normalize(query);
251
+ const haystack = `${plugin.id} ${plugin.name} ${plugin.description || ''} ${(plugin.tags || []).join(' ')}`;
252
+ const h = normalize(haystack);
253
+ // Substring match is enough for a v1 marketplace. v2 can swap in fuse.js.
254
+ return h.includes(n);
255
+ }
256
+
257
+ /**
258
+ * Search the registry. Returns the matching plugins in registry order.
259
+ *
260
+ * @param {string} [query]
261
+ * @param {object} [opts]
262
+ * @param {string} [opts.category] filter by exact category
263
+ * @param {string} [opts.tag] filter by tag (exact)
264
+ * @param {string} [opts.url] registry URL override
265
+ * @returns {Promise<RegistryPlugin[]>}
266
+ */
267
+ export async function searchPlugins(query = '', opts = {}) {
268
+ const registry = await fetchRegistry({ url: opts.url });
269
+ const { category, tag } = opts;
270
+ return registry.plugins.filter((p) => {
271
+ if (category && p.category !== category) return false;
272
+ if (tag && !(Array.isArray(p.tags) && p.tags.includes(tag))) return false;
273
+ if (!matchesQuery(p, query)) return false;
274
+ return true;
275
+ });
276
+ }
277
+
278
+ /**
279
+ * Get a single plugin by id from the registry. Returns null if not found.
280
+ *
281
+ * @param {string} id
282
+ * @param {object} [opts]
283
+ * @param {string} [opts.url]
284
+ * @param {typeof globalThis.fetch} [opts.fetch] override fetch (tests)
285
+ * @returns {Promise<RegistryPlugin | null>}
286
+ */
287
+ export async function getPlugin(id, opts = {}) {
288
+ if (!id || typeof id !== 'string') return null;
289
+ const registry = await fetchRegistry({ url: opts.url, fetch: opts.fetch });
290
+ return registry.plugins.find((p) => p.id === id) || null;
291
+ }
292
+
293
+ /**
294
+ * Stream a file from disk, compute its SHA-256, and compare against the
295
+ * expected `<algo>:<hex>` string. Returns true on match, false on
296
+ * mismatch. Throws on unsupported algorithm or file-read error.
297
+ *
298
+ * The expected format is `sha256:<64-lowercase-hex>`. Other algorithms
299
+ * are rejected up front rather than silently miscomputed.
300
+ *
301
+ * @param {string} filePath
302
+ * @param {string} expected e.g. "sha256:abc..."
303
+ * @returns {Promise<boolean>}
304
+ */
305
+ export function verifyChecksum(filePath, expected) {
306
+ return new Promise((resolveP, rejectP) => {
307
+ if (typeof expected !== 'string' || !expected.includes(':')) {
308
+ rejectP(new Error(`checksum must be <algo>:<hex> (got ${JSON.stringify(expected)})`));
309
+ return;
310
+ }
311
+ const [algo, expectedHex] = expected.split(':', 2);
312
+ if (algo !== 'sha256') {
313
+ rejectP(new Error(`only sha256 checksums are supported (got "${algo}")`));
314
+ return;
315
+ }
316
+ if (!/^[a-f0-9]{64}$/.test(expectedHex)) {
317
+ rejectP(new Error(`sha256 checksum must be 64 lowercase hex chars`));
318
+ return;
319
+ }
320
+ const hash = createHash('sha256');
321
+ const stream = createReadStream(filePath);
322
+ stream.on('data', (chunk) => hash.update(chunk));
323
+ stream.on('error', (err) => rejectP(err));
324
+ stream.on('end', () => {
325
+ const actual = hash.digest('hex');
326
+ resolveP(actual === expectedHex);
327
+ });
328
+ });
329
+ }
330
+
331
+ /**
332
+ * @typedef {{
333
+ * id: string,
334
+ * name: string,
335
+ * version: string,
336
+ * description?: string,
337
+ * author?: string,
338
+ * category?: string,
339
+ * tags?: string[],
340
+ * homepage?: string,
341
+ * tarball: string,
342
+ * checksum: string,
343
+ * permissions: string[],
344
+ * minBizarVersion?: string
345
+ * }} RegistryPlugin
346
+ *
347
+ * @typedef {{
348
+ * version: number,
349
+ * updatedAt: string,
350
+ * plugins: RegistryPlugin[]
351
+ * }} RegistryShape
352
+ */
353
+
354
+ // Export the symbol type aliases for downstream tooling.
355
+ export const __types = /** @type {{
356
+ RegistryPlugin: null,
357
+ RegistryShape: null,
358
+ }} */ ({});
359
+
360
+ export const __testing = {
361
+ CACHE_TTL_MS,
362
+ DEFAULT_REGISTRY_URL,
363
+ };