@polderlabs/bizar 4.9.0 → 5.0.0

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 (107) hide show
  1. package/bizar-dash/dist/assets/icons-CFqu2M-c.js +656 -0
  2. package/bizar-dash/dist/assets/icons-CFqu2M-c.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-Dl8yY5_H.js +16 -0
  6. package/bizar-dash/dist/assets/main-Dl8yY5_H.js.map +1 -0
  7. package/bizar-dash/dist/assets/{main-DfmIfOUS.css → main-ZAfGKENE.css} +1 -1
  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-C2gysFOZ.js +2 -0
  11. package/bizar-dash/dist/assets/mobile-C2gysFOZ.js.map +1 -0
  12. package/bizar-dash/dist/assets/mobile-DHXXbn1A.js +1 -0
  13. package/bizar-dash/dist/assets/{mobile-CL5uUQEC.js.map → mobile-DHXXbn1A.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/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  19. package/bizar-dash/skills/eval/SKILL.md +237 -0
  20. package/bizar-dash/src/server/api.mjs +28 -0
  21. package/bizar-dash/src/server/auth.mjs +155 -1
  22. package/bizar-dash/src/server/eval-store.mjs +226 -0
  23. package/bizar-dash/src/server/eval.mjs +347 -0
  24. package/bizar-dash/src/server/ocr.mjs +55 -0
  25. package/bizar-dash/src/server/plugins/registry.mjs +363 -0
  26. package/bizar-dash/src/server/plugins/sandbox.mjs +655 -0
  27. package/bizar-dash/src/server/plugins/store.mjs +659 -0
  28. package/bizar-dash/src/server/routes/clipboard.mjs +173 -0
  29. package/bizar-dash/src/server/routes/eval.mjs +147 -0
  30. package/bizar-dash/src/server/routes/ocr.mjs +182 -0
  31. package/bizar-dash/src/server/routes/plugins.mjs +220 -0
  32. package/bizar-dash/src/server/routes/users.mjs +84 -0
  33. package/bizar-dash/src/server/routes/voice.mjs +131 -0
  34. package/bizar-dash/src/server/routes/workspaces.mjs +204 -0
  35. package/bizar-dash/src/server/voice-store.mjs +202 -0
  36. package/bizar-dash/src/server/voice-transcribe.mjs +72 -0
  37. package/bizar-dash/src/server/workspaces.mjs +626 -0
  38. package/bizar-dash/src/web/components/InviteDialog.tsx +205 -0
  39. package/bizar-dash/src/web/components/ScreenshotCapture.tsx +138 -0
  40. package/bizar-dash/src/web/components/ScreenshotOCR.tsx +42 -0
  41. package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +182 -0
  42. package/bizar-dash/src/web/components/VoiceRecorder.tsx +104 -0
  43. package/bizar-dash/src/web/components/WorkspaceSelector.tsx +158 -0
  44. package/bizar-dash/src/web/styles/memory.css +82 -0
  45. package/bizar-dash/src/web/views/Memory.tsx +17 -2
  46. package/bizar-dash/src/web/views/Settings.tsx +3 -0
  47. package/bizar-dash/src/web/views/Workspace.tsx +294 -0
  48. package/bizar-dash/src/web/views/memory/FromScreenshotPanel.tsx +23 -0
  49. package/bizar-dash/src/web/views/memory/VaultFromClipboardPanel.tsx +101 -0
  50. package/bizar-dash/src/web/views/settings/WorkspacesSection.tsx +189 -0
  51. package/bizar-dash/tests/bundle-analysis.test.mjs +5 -2
  52. package/bizar-dash/tests/clipboard.test.mjs +147 -0
  53. package/bizar-dash/tests/components/screenshot-ocr.test.tsx +75 -0
  54. package/bizar-dash/tests/components/workspace-selector.test.tsx +73 -0
  55. package/bizar-dash/tests/deploy-templates.test.mjs +100 -0
  56. package/bizar-dash/tests/eval/fixtures.test.mjs +141 -0
  57. package/bizar-dash/tests/eval/report.test.mjs +284 -0
  58. package/bizar-dash/tests/eval/runner.test.mjs +471 -0
  59. package/bizar-dash/tests/ocr.test.mjs +87 -0
  60. package/bizar-dash/tests/plugins-registry.test.mjs +387 -0
  61. package/bizar-dash/tests/plugins-sandbox.test.mjs +374 -0
  62. package/bizar-dash/tests/plugins-store.test.mjs +455 -0
  63. package/bizar-dash/tests/users.test.mjs +108 -0
  64. package/bizar-dash/tests/voice-recorder.test.tsx +95 -0
  65. package/bizar-dash/tests/voice-store.test.mjs +148 -0
  66. package/bizar-dash/tests/voice-transcribe.test.mjs +87 -0
  67. package/bizar-dash/tests/workspaces.test.mjs +527 -0
  68. package/cli/bin.mjs +72 -2
  69. package/cli/commands/clip.mjs +146 -0
  70. package/cli/commands/deploy/cloudflare.mjs +250 -0
  71. package/cli/commands/deploy/docker.mjs +221 -0
  72. package/cli/commands/deploy/fly.mjs +161 -0
  73. package/cli/commands/deploy/vercel.mjs +225 -0
  74. package/cli/commands/deploy.mjs +240 -0
  75. package/cli/commands/eval.mjs +378 -0
  76. package/cli/commands/marketplace.mjs +64 -0
  77. package/cli/commands/ocr.mjs +165 -0
  78. package/cli/commands/plugin.mjs +358 -0
  79. package/cli/commands/voice.mjs +211 -0
  80. package/cli/commands/workspace.mjs +247 -0
  81. package/package.json +12 -8
  82. package/templates/deploy/cloudflare/README.md +32 -0
  83. package/templates/deploy/cloudflare/functions-index.template.js +15 -0
  84. package/templates/deploy/cloudflare/wrangler.toml.template +9 -0
  85. package/templates/deploy/docker/.env.template +16 -0
  86. package/templates/deploy/docker/README.md +58 -0
  87. package/templates/deploy/docker/docker-compose.template.yml +23 -0
  88. package/templates/deploy/fly/README.md +35 -0
  89. package/templates/deploy/fly/fly.toml.template +28 -0
  90. package/templates/deploy/vercel/README.md +29 -0
  91. package/templates/deploy/vercel/api-index.template.js +18 -0
  92. package/templates/deploy/vercel/vercel.json.template +16 -0
  93. package/templates/eval-fixtures/README.md +58 -0
  94. package/templates/eval-fixtures/code-search-basic.json +28 -0
  95. package/templates/eval-fixtures/latency-bounds.json +16 -0
  96. package/templates/eval-fixtures/regression-suite.json +79 -0
  97. package/templates/eval-fixtures/response-format.json +30 -0
  98. package/templates/eval-fixtures/tool-call-correctness.json +24 -0
  99. package/templates/plugin-template/README.md +121 -0
  100. package/templates/plugin-template/index.js +66 -0
  101. package/templates/plugin-template/plugin.json +42 -0
  102. package/templates/plugin-template/tests/plugin.test.js +83 -0
  103. package/bizar-dash/dist/assets/main-DaC1Lc6q.js +0 -366
  104. package/bizar-dash/dist/assets/main-DaC1Lc6q.js.map +0 -1
  105. package/bizar-dash/dist/assets/mobile-CL5uUQEC.js +0 -1
  106. package/bizar-dash/dist/assets/mobile-D5WTWvuh.js +0 -338
  107. package/bizar-dash/dist/assets/mobile-D5WTWvuh.js.map +0 -1
@@ -0,0 +1,387 @@
1
+ /**
2
+ * tests/plugins-registry.test.mjs
3
+ *
4
+ * Tests for src/server/plugins/registry.mjs:
5
+ * - fetch: cache + 1h TTL + force
6
+ * - validate: shape enforcement, missing/invalid fields
7
+ * - search: substring matching + category/tag filters
8
+ * - getPlugin: id lookup + null on miss
9
+ * - verifyChecksum: matching, mismatching, malformed input
10
+ *
11
+ * Strategy: stand up an http.createServer that serves a fixture
12
+ * registry, point the client at it via a fetch override, exercise the
13
+ * surface, then close the server.
14
+ */
15
+ import { test, describe, before, after, beforeEach } from 'node:test';
16
+ import assert from 'node:assert/strict';
17
+ import http from 'node:http';
18
+ import { writeFileSync, mkdirSync, rmSync, readFileSync } from 'node:fs';
19
+ import { join, resolve, dirname } from 'node:path';
20
+ import { tmpdir } from 'node:os';
21
+ import { createHash } from 'node:crypto';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..');
25
+ const REG = await import(resolve(REPO, 'src/server/plugins/registry.mjs'));
26
+
27
+ /**
28
+ * Compute a sha256:<hex> string for a fixture string. We use this
29
+ * to build valid checksums for the tarball verification test.
30
+ */
31
+ function sha256OfString(s) {
32
+ return 'sha256:' + createHash('sha256').update(s).digest('hex');
33
+ }
34
+
35
+ /**
36
+ * Minimal valid registry fixture with two plugins.
37
+ */
38
+ function buildFixture() {
39
+ return {
40
+ version: 1,
41
+ updatedAt: '2026-07-05T00:00:00.000Z',
42
+ plugins: [
43
+ {
44
+ id: 'vercel-deploy',
45
+ name: 'Vercel Deploy',
46
+ version: '1.0.0',
47
+ description: 'Deploy dashboard to Vercel with one command',
48
+ author: 'DrB0rk',
49
+ category: 'deploy',
50
+ tags: ['vercel', 'deploy'],
51
+ homepage: 'https://example.com',
52
+ tarball: 'https://example.com/vercel-deploy-1.0.0.tar.gz',
53
+ checksum: sha256OfString('vercel-tarball-bytes'),
54
+ permissions: ['net', 'fs:read'],
55
+ minBizarVersion: '4.9.0',
56
+ },
57
+ {
58
+ id: 'github-pr-watcher',
59
+ name: 'GitHub PR Watcher',
60
+ version: '2.1.3',
61
+ description: 'Watch pull requests and trigger workflows',
62
+ author: 'community',
63
+ category: 'workflow',
64
+ tags: ['github', 'pr'],
65
+ tarball: 'https://example.com/github-pr-watcher-2.1.3.tar.gz',
66
+ checksum: sha256OfString('github-tarball-bytes'),
67
+ permissions: ['net'],
68
+ },
69
+ ],
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Spin up a tiny HTTP server that returns the fixture on every GET.
75
+ * Returns `{ url, close }`.
76
+ */
77
+ function startServer(fixture) {
78
+ return new Promise((resolveStart) => {
79
+ const server = http.createServer((req, res) => {
80
+ if (req.url === '/registry.json') {
81
+ res.writeHead(200, { 'Content-Type': 'application/json' });
82
+ res.end(JSON.stringify(fixture));
83
+ return;
84
+ }
85
+ res.writeHead(404);
86
+ res.end();
87
+ });
88
+ server.listen(0, '127.0.0.1', () => {
89
+ const { port } = server.address();
90
+ resolveStart({
91
+ url: `http://127.0.0.1:${port}/registry.json`,
92
+ close: () => new Promise((r) => server.close(r)),
93
+ });
94
+ });
95
+ });
96
+ }
97
+
98
+ let server;
99
+ let fixture;
100
+ let url;
101
+
102
+ before(async () => {
103
+ fixture = buildFixture();
104
+ server = await startServer(fixture);
105
+ url = server.url;
106
+ });
107
+
108
+ after(async () => {
109
+ if (server) await server.close();
110
+ });
111
+
112
+ beforeEach(() => {
113
+ REG.__resetCache();
114
+ });
115
+
116
+ // ── fetchRegistry ─────────────────────────────────────────────────────────
117
+
118
+ describe('fetchRegistry', () => {
119
+ test('returns parsed + validated shape', async () => {
120
+ const r = await REG.fetchRegistry({ url, fetch: globalThis.fetch });
121
+ assert.equal(r.version, 1);
122
+ assert.equal(typeof r.updatedAt, 'string');
123
+ assert.equal(r.plugins.length, 2);
124
+ assert.equal(r.plugins[0].id, 'vercel-deploy');
125
+ });
126
+
127
+ test('caches the response (second call hits cache, not server)', async () => {
128
+ // To prove caching, we point at a server that would 500 on a
129
+ // second hit. If caching works, the second call still succeeds.
130
+ let calls = 0;
131
+ const flaky = await new Promise((r) => {
132
+ const s = http.createServer((req, res) => {
133
+ calls += 1;
134
+ if (calls === 1) {
135
+ res.writeHead(200, { 'Content-Type': 'application/json' });
136
+ res.end(JSON.stringify(fixture));
137
+ return;
138
+ }
139
+ res.writeHead(500);
140
+ res.end('boom');
141
+ });
142
+ s.listen(0, '127.0.0.1', () => {
143
+ const { port } = s.address();
144
+ r({ url: `http://127.0.0.1:${port}/registry.json`, close: () => new Promise((rr) => s.close(rr)) });
145
+ });
146
+ });
147
+ try {
148
+ const r1 = await REG.fetchRegistry({ url: flaky.url, fetch: globalThis.fetch });
149
+ assert.equal(r1.plugins.length, 2);
150
+ const r2 = await REG.fetchRegistry({ url: flaky.url, fetch: globalThis.fetch });
151
+ assert.equal(r2.plugins.length, 2);
152
+ assert.equal(calls, 1, 'second call should hit cache, not network');
153
+ } finally {
154
+ await flaky.close();
155
+ }
156
+ });
157
+
158
+ test('force=true bypasses the cache', async () => {
159
+ let calls = 0;
160
+ const counter = await new Promise((r) => {
161
+ const s = http.createServer((req, res) => {
162
+ calls += 1;
163
+ res.writeHead(200, { 'Content-Type': 'application/json' });
164
+ res.end(JSON.stringify(fixture));
165
+ });
166
+ s.listen(0, '127.0.0.1', () => {
167
+ const { port } = s.address();
168
+ r({ url: `http://127.0.0.1:${port}/registry.json`, close: () => new Promise((rr) => s.close(rr)) });
169
+ });
170
+ });
171
+ try {
172
+ await REG.fetchRegistry({ url: counter.url, fetch: globalThis.fetch });
173
+ await REG.fetchRegistry({ url: counter.url, fetch: globalThis.fetch, force: true });
174
+ assert.equal(calls, 2);
175
+ } finally {
176
+ await counter.close();
177
+ }
178
+ });
179
+
180
+ test('throws registry_unreachable on a 500 response', async () => {
181
+ const broken = await new Promise((r) => {
182
+ const s = http.createServer((req, res) => {
183
+ res.writeHead(500);
184
+ res.end('boom');
185
+ });
186
+ s.listen(0, '127.0.0.1', () => {
187
+ const { port } = s.address();
188
+ r({ url: `http://127.0.0.1:${port}/x`, close: () => new Promise((rr) => s.close(rr)) });
189
+ });
190
+ });
191
+ try {
192
+ await assert.rejects(
193
+ () => REG.fetchRegistry({ url: broken.url, fetch: globalThis.fetch }),
194
+ (err) => err.code === 'registry_unreachable',
195
+ );
196
+ } finally {
197
+ await broken.close();
198
+ }
199
+ });
200
+ });
201
+
202
+ // ── validateRegistry ──────────────────────────────────────────────────────
203
+
204
+ describe('validateRegistry', () => {
205
+ test('rejects missing version', () => {
206
+ const bad = { updatedAt: 'x', plugins: [] };
207
+ assert.throws(() => REG.validateRegistry(bad), /version must be 1/);
208
+ });
209
+
210
+ test('rejects non-1 version', () => {
211
+ const bad = { version: 2, updatedAt: 'x', plugins: [] };
212
+ assert.throws(() => REG.validateRegistry(bad), /version must be 1/);
213
+ });
214
+
215
+ test('rejects plugin with missing id', () => {
216
+ const bad = {
217
+ version: 1,
218
+ updatedAt: 'x',
219
+ plugins: [{ name: 'X', version: '1.0.0', tarball: 'https://x', checksum: sha256OfString('x') }],
220
+ };
221
+ assert.throws(() => REG.validateRegistry(bad), /id must be a kebab-case/);
222
+ });
223
+
224
+ test('rejects duplicate plugin ids', () => {
225
+ const bad = {
226
+ version: 1,
227
+ updatedAt: 'x',
228
+ plugins: [
229
+ { id: 'a', name: 'A', version: '1.0.0', tarball: 'https://x', checksum: sha256OfString('x'), permissions: [] },
230
+ { id: 'a', name: 'A2', version: '1.0.0', tarball: 'https://x', checksum: sha256OfString('y'), permissions: [] },
231
+ ],
232
+ };
233
+ assert.throws(() => REG.validateRegistry(bad), /duplicated/);
234
+ });
235
+
236
+ test('rejects non-https tarball', () => {
237
+ const bad = {
238
+ version: 1,
239
+ updatedAt: 'x',
240
+ plugins: [
241
+ { id: 'a', name: 'A', version: '1.0.0', tarball: 'file:///etc/passwd', checksum: sha256OfString('x'), permissions: [] },
242
+ ],
243
+ };
244
+ assert.throws(() => REG.validateRegistry(bad), /http\(s\) URL/);
245
+ });
246
+
247
+ test('rejects malformed checksum', () => {
248
+ const bad = {
249
+ version: 1,
250
+ updatedAt: 'x',
251
+ plugins: [
252
+ { id: 'a', name: 'A', version: '1.0.0', tarball: 'https://x', checksum: 'md5:abc', permissions: [] },
253
+ ],
254
+ };
255
+ assert.throws(() => REG.validateRegistry(bad), /sha256:<64-hex>/);
256
+ });
257
+
258
+ test('rejects non-array permissions', () => {
259
+ const bad = {
260
+ version: 1,
261
+ updatedAt: 'x',
262
+ plugins: [
263
+ { id: 'a', name: 'A', version: '1.0.0', tarball: 'https://x', checksum: sha256OfString('x'), permissions: 'net' },
264
+ ],
265
+ };
266
+ assert.throws(() => REG.validateRegistry(bad), /permissions must be an array/);
267
+ });
268
+
269
+ test('accepts a valid registry', () => {
270
+ const r = REG.validateRegistry(buildFixture());
271
+ assert.equal(r.plugins.length, 2);
272
+ });
273
+ });
274
+
275
+ // ── searchPlugins ─────────────────────────────────────────────────────────
276
+
277
+ describe('searchPlugins', () => {
278
+ test('returns all when query is empty', async () => {
279
+ const r = await REG.searchPlugins('', { url });
280
+ assert.equal(r.length, 2);
281
+ });
282
+
283
+ test('matches against name', async () => {
284
+ const r = await REG.searchPlugins('vercel', { url });
285
+ assert.equal(r.length, 1);
286
+ assert.equal(r[0].id, 'vercel-deploy');
287
+ });
288
+
289
+ test('matches against description', async () => {
290
+ const r = await REG.searchPlugins('pull requests', { url });
291
+ assert.equal(r.length, 1);
292
+ assert.equal(r[0].id, 'github-pr-watcher');
293
+ });
294
+
295
+ test('matches against tags', async () => {
296
+ const r = await REG.searchPlugins('github', { url });
297
+ assert.equal(r.length, 1);
298
+ assert.equal(r[0].id, 'github-pr-watcher');
299
+ });
300
+
301
+ test('category filter', async () => {
302
+ const r = await REG.searchPlugins('', { url, category: 'deploy' });
303
+ assert.equal(r.length, 1);
304
+ assert.equal(r[0].id, 'vercel-deploy');
305
+ });
306
+
307
+ test('tag filter', async () => {
308
+ const r = await REG.searchPlugins('', { url, tag: 'github' });
309
+ assert.equal(r.length, 1);
310
+ assert.equal(r[0].id, 'github-pr-watcher');
311
+ });
312
+
313
+ test('returns [] for a query that matches nothing', async () => {
314
+ const r = await REG.searchPlugins('no-such-thing', { url });
315
+ assert.equal(r.length, 0);
316
+ });
317
+ });
318
+
319
+ // ── getPlugin ─────────────────────────────────────────────────────────────
320
+
321
+ describe('getPlugin', () => {
322
+ test('returns the matching entry', async () => {
323
+ const r = await REG.getPlugin('vercel-deploy', { url });
324
+ assert.ok(r);
325
+ assert.equal(r.name, 'Vercel Deploy');
326
+ });
327
+
328
+ test('returns null on miss', async () => {
329
+ const r = await REG.getPlugin('does-not-exist', { url });
330
+ assert.equal(r, null);
331
+ });
332
+
333
+ test('returns null for empty id', async () => {
334
+ const r = await REG.getPlugin('', { url });
335
+ assert.equal(r, null);
336
+ });
337
+ });
338
+
339
+ // ── verifyChecksum ───────────────────────────────────────────────────────
340
+
341
+ describe('verifyChecksum', () => {
342
+ let tmp;
343
+ before(async () => {
344
+ tmp = join(tmpdir(), `bizar-registry-test-${Date.now()}`);
345
+ mkdirSync(tmp, { recursive: true });
346
+ });
347
+ after(async () => {
348
+ if (tmp) rmSync(tmp, { recursive: true, force: true });
349
+ });
350
+
351
+ test('returns true for a matching checksum', async () => {
352
+ const file = join(tmp, 'a.bin');
353
+ const content = 'hello world';
354
+ writeFileSync(file, content);
355
+ const expected = sha256OfString(content);
356
+ const ok = await REG.verifyChecksum(file, expected);
357
+ assert.equal(ok, true);
358
+ });
359
+
360
+ test('returns false for a mismatched checksum', async () => {
361
+ const file = join(tmp, 'b.bin');
362
+ writeFileSync(file, 'something else entirely');
363
+ const ok = await REG.verifyChecksum(file, sha256OfString('not this'));
364
+ assert.equal(ok, false);
365
+ });
366
+
367
+ test('rejects non-sha256 algorithms', async () => {
368
+ const file = join(tmp, 'c.bin');
369
+ writeFileSync(file, 'whatever');
370
+ await assert.rejects(() => REG.verifyChecksum(file, 'md5:abc'), /only sha256/);
371
+ });
372
+
373
+ test('rejects malformed checksum strings', async () => {
374
+ const file = join(tmp, 'd.bin');
375
+ writeFileSync(file, 'whatever');
376
+ await assert.rejects(() => REG.verifyChecksum(file, 'not-a-checksum'), /<algo>:<hex>/);
377
+ });
378
+
379
+ test('rejects bad hex length', async () => {
380
+ const file = join(tmp, 'e.bin');
381
+ writeFileSync(file, 'whatever');
382
+ await assert.rejects(
383
+ () => REG.verifyChecksum(file, 'sha256:abc'),
384
+ /64 lowercase hex chars/,
385
+ );
386
+ });
387
+ });