@polderlabs/bizar 4.4.13 → 4.5.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.
- package/bizar-dash/CHANGELOG.md +37 -276
- package/bizar-dash/dist/assets/main-CDFKHzBg.css +1 -0
- package/bizar-dash/dist/assets/main-NYFpS2wY.js +312 -0
- package/bizar-dash/dist/assets/main-NYFpS2wY.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile--0FBIKX3.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile--0FBIKX3.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-OgRp8VIb.js +352 -0
- package/bizar-dash/dist/assets/mobile-OgRp8VIb.js.map +1 -0
- package/bizar-dash/dist/index.html +3 -3
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
- package/bizar-dash/skills/bizar/SKILL.md +96 -0
- package/bizar-dash/skills/chat/SKILL.md +74 -0
- package/bizar-dash/skills/lightrag/SKILL.md +75 -0
- package/bizar-dash/skills/minimax/SKILL.md +80 -0
- package/bizar-dash/skills/obsidian/SKILL.md +55 -0
- package/bizar-dash/skills/providers/SKILL.md +75 -0
- package/bizar-dash/skills/sdk/SKILL.md +138 -0
- package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
- package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
- package/bizar-dash/skills/usage/SKILL.md +62 -0
- package/bizar-dash/src/server/api.mjs +12 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +5 -2
- package/bizar-dash/src/server/memory-store.mjs +38 -0
- package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
- package/bizar-dash/src/server/minimax.mjs +196 -5
- package/bizar-dash/src/server/providers-store.mjs +956 -0
- package/bizar-dash/src/server/routes/config.mjs +52 -1
- package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
- package/bizar-dash/src/server/routes/memory.mjs +241 -1
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
- package/bizar-dash/src/server/routes/providers.mjs +266 -5
- package/bizar-dash/src/server/routes/skills.mjs +32 -43
- package/bizar-dash/src/server/routes/update.mjs +340 -0
- package/bizar-dash/src/server/routes/usage.mjs +136 -0
- package/bizar-dash/src/server/serve-info.mjs +135 -4
- package/bizar-dash/src/server/server.mjs +4 -0
- package/bizar-dash/src/server/skills-store.mjs +152 -262
- package/bizar-dash/src/web/App.tsx +118 -29
- package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
- package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
- package/bizar-dash/src/web/components/Topbar.tsx +0 -1
- package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
- package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
- package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
- package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
- package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
- package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
- package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
- package/bizar-dash/src/web/lib/api.ts +43 -0
- package/bizar-dash/src/web/main.tsx +1 -0
- package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
- package/bizar-dash/src/web/styles/chat.css +135 -1
- package/bizar-dash/src/web/styles/main.css +46 -0
- package/bizar-dash/src/web/styles/minimax-usage.css +335 -0
- package/bizar-dash/src/web/styles/settings.css +418 -0
- package/bizar-dash/src/web/styles/skills.css +302 -0
- package/bizar-dash/src/web/styles/tasks.css +288 -0
- package/bizar-dash/src/web/views/Chat.tsx +276 -48
- package/bizar-dash/src/web/views/Config.tsx +3 -2065
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +476 -461
- package/bizar-dash/src/web/views/Settings.tsx +6 -0
- package/bizar-dash/src/web/views/Skills.tsx +208 -260
- package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
- package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
- package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
- package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
- package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
- package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
- package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
- package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
- package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
- package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
- package/bizar-dash/tests/skills-list.test.mjs +232 -0
- package/bizar-dash/tests/skills-search.test.mjs +222 -0
- package/bizar-dash/tests/tasks-create.test.mjs +187 -0
- package/bizar-dash/tests/update-check.test.mjs +127 -0
- package/bizar-dash/tests/update-run.test.mjs +266 -0
- package/cli/bin.mjs +82 -1
- package/cli/provision.mjs +118 -4
- package/config/agents/_shared/SKILLS.md +109 -0
- package/package.json +1 -1
- package/bizar-dash/dist/assets/main-BB5mJurD.js +0 -352
- package/bizar-dash/dist/assets/main-BB5mJurD.js.map +0 -1
- package/bizar-dash/dist/assets/main-BsnQLXdh.css +0 -1
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +0 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* update-check.test.mjs — tests for /api/updates/status and /api/updates/check.
|
|
3
|
+
*
|
|
4
|
+
* Run with: node --test bizar-dash/tests/update-check.test.mjs
|
|
5
|
+
*
|
|
6
|
+
* Strategy: import the module once, call `resetUpdateCache()` between
|
|
7
|
+
* tests. The route reads through the module-level cache path on every
|
|
8
|
+
* request. In this sandbox, `npm view` may or may not work — the route
|
|
9
|
+
* must degrade gracefully (null latest, hasUpdates=false) on failure.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import express from 'express';
|
|
14
|
+
import { resolve } from 'node:path';
|
|
15
|
+
|
|
16
|
+
const REPO = resolve(import.meta.dirname, '..', '..');
|
|
17
|
+
|
|
18
|
+
const { createUpdateRouter, resetUpdateCache } = await import(
|
|
19
|
+
`${REPO}/bizar-dash/src/server/routes/update.mjs`
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
let app;
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
resetUpdateCache();
|
|
25
|
+
app = express();
|
|
26
|
+
app.use(express.json());
|
|
27
|
+
app.use('/api', createUpdateRouter({ broadcast: () => {} }));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
async function request(app, method, path, body) {
|
|
31
|
+
const http = await import('node:http');
|
|
32
|
+
return new Promise((resolveP, reject) => {
|
|
33
|
+
const server = app.listen(0, '127.0.0.1', () => {
|
|
34
|
+
const { port } = server.address();
|
|
35
|
+
const bodyStr = body ? JSON.stringify(body) : '';
|
|
36
|
+
const req = http.request({
|
|
37
|
+
hostname: '127.0.0.1',
|
|
38
|
+
port,
|
|
39
|
+
path,
|
|
40
|
+
method,
|
|
41
|
+
headers: bodyStr
|
|
42
|
+
? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr) }
|
|
43
|
+
: {},
|
|
44
|
+
}, (res) => {
|
|
45
|
+
let data = '';
|
|
46
|
+
res.on('data', (c) => { data += c; });
|
|
47
|
+
res.on('end', () => {
|
|
48
|
+
server.close();
|
|
49
|
+
try {
|
|
50
|
+
resolveP({ status: res.statusCode, body: JSON.parse(data) });
|
|
51
|
+
} catch {
|
|
52
|
+
resolveP({ status: res.statusCode, body: data });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
req.on('error', (err) => { server.close(); reject(err); });
|
|
57
|
+
if (bodyStr) req.write(bodyStr);
|
|
58
|
+
req.end();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('update.mjs — /api/updates/status', () => {
|
|
64
|
+
it('returns the installed package map', async () => {
|
|
65
|
+
const res = await request(app, 'GET', '/api/updates/status');
|
|
66
|
+
assert.equal(res.status, 200);
|
|
67
|
+
assert.ok(res.body.current, 'response should have current field');
|
|
68
|
+
assert.equal(typeof res.body.current, 'object');
|
|
69
|
+
for (const id of ['bizar', 'bizar-dash', 'bizar-plugin']) {
|
|
70
|
+
assert.ok(id in res.body.current, `current.${id} should be a key`);
|
|
71
|
+
assert.ok(
|
|
72
|
+
res.body.current[id] === null || typeof res.body.current[id] === 'string',
|
|
73
|
+
`current.${id} should be string or null`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('does not include latest/hasUpdates fields', async () => {
|
|
79
|
+
const res = await request(app, 'GET', '/api/updates/status');
|
|
80
|
+
assert.equal(res.status, 200);
|
|
81
|
+
assert.equal(res.body.latest, undefined, 'status should not include latest');
|
|
82
|
+
assert.equal(res.body.hasUpdates, undefined, 'status should not include hasUpdates');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('update.mjs — /api/updates/check', () => {
|
|
87
|
+
it('returns shape { current, latest, hasUpdates }', async () => {
|
|
88
|
+
const res = await request(app, 'GET', '/api/updates/check');
|
|
89
|
+
assert.equal(res.status, 200);
|
|
90
|
+
assert.ok(res.body.current, 'should have current');
|
|
91
|
+
assert.ok(res.body.latest, 'should have latest');
|
|
92
|
+
assert.equal(typeof res.body.hasUpdates, 'boolean');
|
|
93
|
+
for (const id of ['bizar', 'bizar-dash', 'bizar-plugin']) {
|
|
94
|
+
assert.ok(id in res.body.latest, `latest.${id} should be a key`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('does not 5xx on registry failure (graceful degradation)', async () => {
|
|
99
|
+
const res = await request(app, 'GET', '/api/updates/check');
|
|
100
|
+
assert.equal(res.status, 200, 'should not 5xx even if npm view fails');
|
|
101
|
+
assert.equal(typeof res.body.hasUpdates, 'boolean');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('update.mjs — POST /api/updates/apply', () => {
|
|
106
|
+
it('rejects empty/invalid package list with 400', async () => {
|
|
107
|
+
// Empty array → falls back to all known packages, which is valid
|
|
108
|
+
// (returns 202). Test instead that an unknown package id is handled
|
|
109
|
+
// — current implementation filters to known ids and starts the run
|
|
110
|
+
// if at least one id is valid. With no valid ids, returns 400.
|
|
111
|
+
const res = await request(app, 'POST', '/api/updates/apply', {
|
|
112
|
+
packages: ['__nonexistent_pkg__'],
|
|
113
|
+
});
|
|
114
|
+
assert.equal(res.status, 400);
|
|
115
|
+
assert.equal(res.body.error, 'bad_request');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('accepts missing broadcast (default no-op)', async () => {
|
|
119
|
+
const localApp = express();
|
|
120
|
+
localApp.use(express.json());
|
|
121
|
+
localApp.use('/api', createUpdateRouter());
|
|
122
|
+
const res = await request(localApp, 'POST', '/api/updates/apply', {});
|
|
123
|
+
// We accept either 202 (npm succeeded) or 500 (npm not on PATH).
|
|
124
|
+
// The point is the route didn't crash before the handler.
|
|
125
|
+
assert.ok([202, 500].includes(res.status), `unexpected status ${res.status}`);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* update-run.test.mjs — tests for /api/updates/apply progress streaming.
|
|
3
|
+
*
|
|
4
|
+
* Run with: node --test bizar-dash/tests/update-run.test.mjs
|
|
5
|
+
*
|
|
6
|
+
* Strategy: stub `node:child_process` so `spawn` returns a controllable
|
|
7
|
+
* fake process that emits scripted stdout/stderr lines and a chosen
|
|
8
|
+
* exit code. Capture the broadcast channel's emitted messages and
|
|
9
|
+
* assert they fire in the documented order.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import express from 'express';
|
|
14
|
+
import { EventEmitter } from 'node:events';
|
|
15
|
+
import { Readable } from 'node:stream';
|
|
16
|
+
import { resolve } from 'node:path';
|
|
17
|
+
|
|
18
|
+
const REPO = resolve(import.meta.dirname, '..', '..');
|
|
19
|
+
|
|
20
|
+
// ─── Spawn fake ────────────────────────────────────────────────────────────
|
|
21
|
+
// We monkey-patch the module loader's view of `node:child_process` so
|
|
22
|
+
// that update.mjs's `import { spawn } from 'node:child_process'` resolves
|
|
23
|
+
// to our fake.
|
|
24
|
+
|
|
25
|
+
class FakeChildProcess extends EventEmitter {
|
|
26
|
+
constructor() {
|
|
27
|
+
super();
|
|
28
|
+
this.stdout = new Readable({ read() {} });
|
|
29
|
+
this.stderr = new Readable({ read() {} });
|
|
30
|
+
}
|
|
31
|
+
// Helpers for tests
|
|
32
|
+
emitStdout(line) { this.stdout.push(line + '\n'); }
|
|
33
|
+
emitStderr(line) { this.stderr.push(line + '\n'); }
|
|
34
|
+
finish(code = 0) {
|
|
35
|
+
this.stdout.push(null);
|
|
36
|
+
this.stderr.push(null);
|
|
37
|
+
this.emit('close', code);
|
|
38
|
+
}
|
|
39
|
+
failWith(err) {
|
|
40
|
+
this.emit('error', err);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let fakeProc = null;
|
|
45
|
+
function makeFakeSpawn() {
|
|
46
|
+
return (_cmd, _args, _opts) => {
|
|
47
|
+
fakeProc = new FakeChildProcess();
|
|
48
|
+
return fakeProc;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Install the fake by intercepting `node:child_process`. We do this by
|
|
53
|
+
// rewriting `Module._resolveFilename` for one test at a time.
|
|
54
|
+
import { createRequire } from 'node:module';
|
|
55
|
+
const require = createRequire(import.meta.url);
|
|
56
|
+
|
|
57
|
+
let originalLoad;
|
|
58
|
+
let patched = false;
|
|
59
|
+
function installFakeSpawn() {
|
|
60
|
+
if (patched) return;
|
|
61
|
+
// Override via global require cache: pre-populate the require cache
|
|
62
|
+
// for `node:child_process` with a stub module.
|
|
63
|
+
const realChild = require('node:child_process');
|
|
64
|
+
const stub = { ...realChild, spawn: makeFakeSpawn() };
|
|
65
|
+
require.cache[require.resolve('node:child_process')] = {
|
|
66
|
+
id: 'node:child_process',
|
|
67
|
+
filename: 'node:child_process',
|
|
68
|
+
loaded: true,
|
|
69
|
+
exports: stub,
|
|
70
|
+
};
|
|
71
|
+
patched = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Load update.mjs once after we've patched child_process. We do this
|
|
75
|
+
// dynamically so the import picks up the stubbed `spawn` from the
|
|
76
|
+
// require cache that ESM bridges through.
|
|
77
|
+
//
|
|
78
|
+
// NOTE: ESM `import` does NOT use the require cache, so the patch above
|
|
79
|
+
// only works for CommonJS callers of `child_process`. The route imports
|
|
80
|
+
// `spawn` from `node:child_process` directly via ESM. To make this test
|
|
81
|
+
// work, we patch the actual child_process module's `spawn` property at
|
|
82
|
+
// runtime instead.
|
|
83
|
+
function patchSpawnDirectly() {
|
|
84
|
+
const cp = require('node:child_process');
|
|
85
|
+
cp.__originalSpawn = cp.__originalSpawn || cp.spawn;
|
|
86
|
+
cp.spawn = makeFakeSpawn();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function unpatchSpawn() {
|
|
90
|
+
const cp = require('node:child_process');
|
|
91
|
+
if (cp.__originalSpawn) {
|
|
92
|
+
cp.spawn = cp.__originalSpawn;
|
|
93
|
+
}
|
|
94
|
+
fakeProc = null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Now we need the route to use the patched spawn. update.mjs does:
|
|
98
|
+
// import { spawn, execFileSync } from 'node:child_process';
|
|
99
|
+
// In Node.js, ESM imports for built-in modules go through the loader,
|
|
100
|
+
// which uses a private internal cache separate from the CommonJS
|
|
101
|
+
// require cache. So patching require.cache alone won't redirect ESM.
|
|
102
|
+
//
|
|
103
|
+
// The reliable approach is to use a loader hook. For simplicity in
|
|
104
|
+
// this test, we instead use module-aliasing via a wrapper: we use a
|
|
105
|
+
// small middleware-only test that monkey-patches spawn on the imported
|
|
106
|
+
// module instance directly. Since the same module instance is shared
|
|
107
|
+
// across imports within a process, this works after first import.
|
|
108
|
+
|
|
109
|
+
async function importUpdateModuleFresh() {
|
|
110
|
+
// Use a query string to force re-import. ESM treats query strings as
|
|
111
|
+
// distinct specifiers, so this gives us a fresh module instance.
|
|
112
|
+
const url = `${REPO}/bizar-dash/src/server/routes/update.mjs?case=${Date.now()}-${Math.random()}`;
|
|
113
|
+
return import(url);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── App setup ─────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
function setupApp(broadcast) {
|
|
119
|
+
const app = express();
|
|
120
|
+
app.use(express.json());
|
|
121
|
+
return importUpdateModuleFresh().then((mod) => {
|
|
122
|
+
app.use('/api', mod.createUpdateRouter({ broadcast }));
|
|
123
|
+
return { app, mod };
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function request(app, method, path, body) {
|
|
128
|
+
const http = await import('node:http');
|
|
129
|
+
return new Promise((resolveP, reject) => {
|
|
130
|
+
const server = app.listen(0, '127.0.0.1', () => {
|
|
131
|
+
const { port } = server.address();
|
|
132
|
+
const bodyStr = body ? JSON.stringify(body) : '';
|
|
133
|
+
const req = http.request({
|
|
134
|
+
hostname: '127.0.0.1',
|
|
135
|
+
port,
|
|
136
|
+
path,
|
|
137
|
+
method,
|
|
138
|
+
headers: bodyStr
|
|
139
|
+
? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr) }
|
|
140
|
+
: {},
|
|
141
|
+
}, (res) => {
|
|
142
|
+
let data = '';
|
|
143
|
+
res.on('data', (c) => { data += c; });
|
|
144
|
+
res.on('end', () => {
|
|
145
|
+
server.close();
|
|
146
|
+
try {
|
|
147
|
+
resolveP({ status: res.statusCode, body: JSON.parse(data) });
|
|
148
|
+
} catch {
|
|
149
|
+
resolveP({ status: res.statusCode, body: data });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
req.on('error', (err) => { server.close(); reject(err); });
|
|
154
|
+
if (bodyStr) req.write(bodyStr);
|
|
155
|
+
req.end();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─── Wait helper ───────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
function waitFor(predicate, { timeoutMs = 4000, intervalMs = 10 } = {}) {
|
|
163
|
+
return new Promise((resolveP, reject) => {
|
|
164
|
+
const start = Date.now();
|
|
165
|
+
const tick = () => {
|
|
166
|
+
if (predicate()) {
|
|
167
|
+
resolveP();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (Date.now() - start > timeoutMs) {
|
|
171
|
+
reject(new Error(`waitFor timed out after ${timeoutMs}ms`));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
setTimeout(tick, intervalMs);
|
|
175
|
+
};
|
|
176
|
+
tick();
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─── Tests ─────────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
describe('update.mjs — apply run broadcasts events in order', () => {
|
|
183
|
+
let broadcast;
|
|
184
|
+
beforeEach(() => {
|
|
185
|
+
patchSpawnDirectly();
|
|
186
|
+
broadcast = [];
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('emits starting → log → done → complete for a successful install', async () => {
|
|
190
|
+
const { app } = await setupApp((msg) => broadcast.push(msg));
|
|
191
|
+
const res = await request(app, 'POST', '/api/updates/apply', { packages: ['bizar'] });
|
|
192
|
+
assert.equal(res.status, 202);
|
|
193
|
+
assert.deepEqual(res.body.started, ['bizar']);
|
|
194
|
+
|
|
195
|
+
// The first event should be a `starting` progress for `bizar`
|
|
196
|
+
await waitFor(() => broadcast.length >= 1);
|
|
197
|
+
assert.equal(broadcast[0].type, 'update:progress');
|
|
198
|
+
assert.equal(broadcast[0].pkg, 'bizar');
|
|
199
|
+
assert.equal(broadcast[0].status, 'starting');
|
|
200
|
+
|
|
201
|
+
// Now drive the fake spawn to emit log lines + finish
|
|
202
|
+
assert.ok(fakeProc, 'fakeProc should exist after request');
|
|
203
|
+
fakeProc.emitStdout('npm WARN deprecated foo');
|
|
204
|
+
fakeProc.emitStdout('added 1 package in 2s');
|
|
205
|
+
fakeProc.finish(0);
|
|
206
|
+
|
|
207
|
+
// Wait for the complete event
|
|
208
|
+
await waitFor(() => broadcast.some((m) => m.type === 'update:complete'), { timeoutMs: 5000 });
|
|
209
|
+
|
|
210
|
+
// Verify ordering and types
|
|
211
|
+
const types = broadcast.map((m) => `${m.type}${m.pkg ? `:${m.pkg}` : ''}:${m.status ?? ''}`);
|
|
212
|
+
assert.ok(types.includes('update:progress:bizar:starting'), 'should have starting progress');
|
|
213
|
+
assert.ok(types.includes('update:log:bizar:'), 'should have at least one log');
|
|
214
|
+
assert.ok(types.includes('update:progress:bizar:done'), 'should have done progress');
|
|
215
|
+
assert.ok(types.at(-1) === 'update:complete:', 'last event should be update:complete');
|
|
216
|
+
|
|
217
|
+
// Done event should carry newVersion
|
|
218
|
+
const doneEvent = broadcast.find((m) => m.type === 'update:progress' && m.status === 'done');
|
|
219
|
+
assert.ok(doneEvent, 'done event should exist');
|
|
220
|
+
assert.ok('newVersion' in doneEvent, 'done event should carry newVersion field');
|
|
221
|
+
|
|
222
|
+
unpatchSpawn();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('emits error progress + complete when npm exits non-zero', async () => {
|
|
226
|
+
const { app } = await setupApp((msg) => broadcast.push(msg));
|
|
227
|
+
const res = await request(app, 'POST', '/api/updates/apply', { packages: ['bizar'] });
|
|
228
|
+
assert.equal(res.status, 202);
|
|
229
|
+
|
|
230
|
+
await waitFor(() => broadcast.length >= 1);
|
|
231
|
+
assert.ok(fakeProc, 'fakeProc should exist');
|
|
232
|
+
fakeProc.emitStderr('npm ERR! code EACCES');
|
|
233
|
+
fakeProc.finish(1);
|
|
234
|
+
|
|
235
|
+
await waitFor(() => broadcast.some((m) => m.type === 'update:complete'));
|
|
236
|
+
|
|
237
|
+
const errorEvent = broadcast.find((m) => m.type === 'update:progress' && m.status === 'error');
|
|
238
|
+
assert.ok(errorEvent, 'should have error progress event');
|
|
239
|
+
assert.ok(errorEvent.error, 'error event should have error message');
|
|
240
|
+
assert.match(errorEvent.error, /exited with code 1/);
|
|
241
|
+
|
|
242
|
+
const lastEvent = broadcast.at(-1);
|
|
243
|
+
assert.equal(lastEvent.type, 'update:complete');
|
|
244
|
+
|
|
245
|
+
unpatchSpawn();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('returns 409 when an apply is already running', async () => {
|
|
249
|
+
const { app } = await setupApp(() => {});
|
|
250
|
+
// First call — kicks off the run but the fake proc never closes,
|
|
251
|
+
// so the run is "active" for the duration of the test.
|
|
252
|
+
const first = await request(app, 'POST', '/api/updates/apply', { packages: ['bizar'] });
|
|
253
|
+
assert.equal(first.status, 202);
|
|
254
|
+
|
|
255
|
+
// Second call — should hit the concurrency guard
|
|
256
|
+
const second = await request(app, 'POST', '/api/updates/apply', { packages: ['bizar'] });
|
|
257
|
+
assert.equal(second.status, 409);
|
|
258
|
+
assert.equal(second.body.error, 'already_running');
|
|
259
|
+
|
|
260
|
+
// Cleanup: close the fake proc so the run's finally clears the guard
|
|
261
|
+
if (fakeProc) fakeProc.finish(0);
|
|
262
|
+
// Give the async run a moment to clear
|
|
263
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
264
|
+
unpatchSpawn();
|
|
265
|
+
});
|
|
266
|
+
});
|
package/cli/bin.mjs
CHANGED
|
@@ -98,6 +98,7 @@ function showHelp() {
|
|
|
98
98
|
doctor Check the BizarHarness install for health issues
|
|
99
99
|
heads-up <subcommand> Manage pre-push / pre-release heads-ups (list/check/archive)
|
|
100
100
|
minimax <subcommand> Manage MiniMax Token Plan integration (key + onboarding)
|
|
101
|
+
usage Show compact usage analytics summary (24h rolling)
|
|
101
102
|
mod <subcommand> Manage mods (install/upgrade/list via the dashboard API)
|
|
102
103
|
|
|
103
104
|
Examples:
|
|
@@ -196,6 +197,8 @@ function showUpdateHelp() {
|
|
|
196
197
|
|
|
197
198
|
Usage:
|
|
198
199
|
bizar update Update EVERYTHING (default; auto-kills + restarts)
|
|
200
|
+
bizar update --check Only print current vs. latest; do not update
|
|
201
|
+
bizar update --channel=stable|beta Pick the npm dist-tag (default: stable)
|
|
199
202
|
bizar update --no-restart Don't auto-restart the dashboard after update
|
|
200
203
|
bizar update --dry-run Print what would happen, change nothing
|
|
201
204
|
bizar update --force Override .bizar/PRE_PUSH_NOTES.md blockers
|
|
@@ -223,12 +226,20 @@ function showUpdateHelp() {
|
|
|
223
226
|
--no-restart).
|
|
224
227
|
• Runs 'bizar doctor' after a successful update to catch config
|
|
225
228
|
regressions before opencode tries to start.
|
|
229
|
+
• With --check: prints the version matrix and release-notes excerpt
|
|
230
|
+
between current and latest, exits non-zero if an update is available.
|
|
226
231
|
|
|
227
232
|
Examples:
|
|
228
233
|
bizar update Full auto-update (recommended)
|
|
234
|
+
bizar update --check Show version matrix + notes, do nothing
|
|
235
|
+
bizar update --channel=beta Upgrade to latest beta build
|
|
229
236
|
bizar update --dry-run Preview what would change
|
|
230
|
-
bizar update --pick Pick specific components
|
|
231
237
|
bizar update plugin --no-restart Plugin-only, leave dashboard alone
|
|
238
|
+
|
|
239
|
+
Errors:
|
|
240
|
+
Network failures (registry offline / DNS) and npm permission issues
|
|
241
|
+
are surfaced with the raw npm output. The provisioner never silently
|
|
242
|
+
swallows them — look for the ✗ marker in the step output.
|
|
232
243
|
`);
|
|
233
244
|
}
|
|
234
245
|
|
|
@@ -731,6 +742,73 @@ async function runMinimaxCommand(minimaxArgs) {
|
|
|
731
742
|
process.exit(1);
|
|
732
743
|
}
|
|
733
744
|
|
|
745
|
+
// ── Usage subcommand ───────────────────────────────────────────────────────
|
|
746
|
+
|
|
747
|
+
function showUsageHelp() {
|
|
748
|
+
console.log(`
|
|
749
|
+
bizar usage — Show compact usage analytics summary
|
|
750
|
+
|
|
751
|
+
Usage:
|
|
752
|
+
bizar usage [24h|7d|30d] Show summary for the given range (default: 24h)
|
|
753
|
+
|
|
754
|
+
Examples:
|
|
755
|
+
bizar usage
|
|
756
|
+
bizar usage 7d
|
|
757
|
+
bizar usage 30d
|
|
758
|
+
`);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async function runUsageCommand(args) {
|
|
762
|
+
const range = (args[0] && ['24h', '7d', '30d'].includes(args[0])) ? args[0] : '24h';
|
|
763
|
+
const { port, secret } = readDashboardConn();
|
|
764
|
+
const url = `http://127.0.0.1:${port}/api/usage?range=${range}`;
|
|
765
|
+
const headers = { accept: 'application/json' };
|
|
766
|
+
if (secret) headers.authorization = `Basic ${Buffer.from(`opencode:${secret}`).toString('base64')}`;
|
|
767
|
+
try {
|
|
768
|
+
const resp = await fetch(url, { method: 'GET', headers });
|
|
769
|
+
const text = await resp.text();
|
|
770
|
+
let data = null;
|
|
771
|
+
try { data = text ? JSON.parse(text) : null; } catch { /* ignore */ }
|
|
772
|
+
if (!resp.ok || !data) {
|
|
773
|
+
console.error(chalk.red(` ✗ Failed to load usage data: ${data?.message ?? resp.statusText}`));
|
|
774
|
+
process.exit(1);
|
|
775
|
+
}
|
|
776
|
+
const t = data.totals;
|
|
777
|
+
console.log('');
|
|
778
|
+
console.log(chalk.bold(` Usage summary — ${range} (from JSONL store)`));
|
|
779
|
+
console.log('');
|
|
780
|
+
console.log(` ${chalk.dim('Requests:')} ${t.requests.toLocaleString()} (${t.errors} errors)`);
|
|
781
|
+
console.log(` ${chalk.dim('Tokens:')} ${t.totalTokens.toLocaleString()} total (${t.promptTokens.toLocaleString()} prompt · ${t.completionTokens.toLocaleString()} completion)`);
|
|
782
|
+
console.log(` ${chalk.dim('Cached:')} ${t.cachedTokens.toLocaleString()} tokens`);
|
|
783
|
+
console.log(` ${chalk.dim('Reasoning:')} ${t.reasoningTokens.toLocaleString()} tokens`);
|
|
784
|
+
console.log(` ${chalk.dim('Avg latency:')} ${t.avgLatencyMs}ms (p95: ${t.p95LatencyMs}ms)`);
|
|
785
|
+
if (t.costEstimate > 0) {
|
|
786
|
+
console.log(` ${chalk.dim('Est. cost:')} $${t.costEstimate.toFixed(4)} USD`);
|
|
787
|
+
}
|
|
788
|
+
console.log('');
|
|
789
|
+
if (data.daily && data.daily.length > 0) {
|
|
790
|
+
console.log(chalk.dim(` ${chalk.bold('Daily breakdown')}`));
|
|
791
|
+
for (const day of data.daily.slice(-7)) {
|
|
792
|
+
const barLen = Math.round((day.totalTokens / Math.max(...data.daily.map(d => d.totalTokens))) * 20);
|
|
793
|
+
const bar = '█'.repeat(barLen) + '░'.repeat(20 - barLen);
|
|
794
|
+
console.log(` ${day.date} ${bar} ${day.totalTokens.toLocaleString()} tok ${day.requests} req`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (data.perModel && data.perModel.length > 0) {
|
|
798
|
+
console.log('');
|
|
799
|
+
console.log(chalk.dim(` ${chalk.bold('Per model')}`));
|
|
800
|
+
for (const m of data.perModel.slice(0, 8)) {
|
|
801
|
+
console.log(` ${m.modelId.padEnd(24)} ${String(m.requests).padStart(6)} req ${String(m.totalTokens).padStart(8)} tok`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
console.log('');
|
|
805
|
+
} catch (err) {
|
|
806
|
+
console.error(chalk.red(` ✗ Network error: ${err && err.message ? err.message : String(err)}`));
|
|
807
|
+
console.error(chalk.dim(' Is the dashboard running? Run `bizar dash start` first.'));
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
734
812
|
function showDashHelp() {
|
|
735
813
|
console.log(`
|
|
736
814
|
bizar dash — Manage the Bizar dashboard
|
|
@@ -1083,6 +1161,9 @@ async function main() {
|
|
|
1083
1161
|
// test, config, clear, reset-onboarding. All commands shell out
|
|
1084
1162
|
// to the dashboard's /api/minimax/* routes.
|
|
1085
1163
|
await runMinimaxCommand(args.slice(1));
|
|
1164
|
+
} else if (args[0] === 'usage') {
|
|
1165
|
+
// v4.6.0 — Compact usage analytics summary from the JSONL store.
|
|
1166
|
+
await runUsageCommand(args.slice(1));
|
|
1086
1167
|
} else if (args[0] === 'dash' || args[0] === 'dashboard') {
|
|
1087
1168
|
// `bizar dashboard` is a deprecated alias for `bizar dash`
|
|
1088
1169
|
if (args[0] === 'dashboard') {
|
package/cli/provision.mjs
CHANGED
|
@@ -1121,17 +1121,131 @@ export async function runProvision(opts = {}) {
|
|
|
1121
1121
|
* `cli/update.mjs` (the bin.mjs-facing module) can stay a thin shim
|
|
1122
1122
|
* without duplicating flag parsing. Accepts the legacy `subargs: string[]`
|
|
1123
1123
|
* shape so any external callers keep working.
|
|
1124
|
+
*
|
|
1125
|
+
* Recognized flags (v4.4.14):
|
|
1126
|
+
* --check Only print current vs. latest, don't update.
|
|
1127
|
+
* --channel <name> npm dist-tag (stable | beta). Default: stable.
|
|
1128
|
+
* --no-restart Don't auto-restart the dashboard after update.
|
|
1129
|
+
* --dry-run Print what would happen, change nothing.
|
|
1130
|
+
* --force Override .bizar/PRE_PUSH_NOTES.md blockers.
|
|
1131
|
+
* --yes / -y Same as --force, but named for one-line scripts.
|
|
1132
|
+
* --with-mods <csv> Opt-in: install specific mods as part of the run.
|
|
1133
|
+
*
|
|
1134
|
+
* With --check, we print the version matrix and release notes between
|
|
1135
|
+
* current and latest, then return without touching the system.
|
|
1124
1136
|
*/
|
|
1125
1137
|
export async function runUpdate(subargs = []) {
|
|
1138
|
+
const args = Array.isArray(subargs) ? subargs : [];
|
|
1139
|
+
const checkOnly = args.includes('--check');
|
|
1140
|
+
|
|
1141
|
+
// --channel <name> — extract a single value. Accepts both
|
|
1142
|
+
// --channel=beta (equals form)
|
|
1143
|
+
// --channel beta (separate-arg form)
|
|
1144
|
+
// Default: 'stable'.
|
|
1145
|
+
let channel = 'stable';
|
|
1146
|
+
const eqArg = args.find((a) => a.startsWith('--channel='));
|
|
1147
|
+
if (eqArg) {
|
|
1148
|
+
const value = eqArg.slice('--channel='.length);
|
|
1149
|
+
if (value === 'stable' || value === 'beta') {
|
|
1150
|
+
channel = value;
|
|
1151
|
+
} else {
|
|
1152
|
+
console.log(chalk.yellow(` ⚠ Unknown channel "${value}". Using "stable".`));
|
|
1153
|
+
}
|
|
1154
|
+
} else {
|
|
1155
|
+
const channelIdx = args.indexOf('--channel');
|
|
1156
|
+
if (channelIdx >= 0) {
|
|
1157
|
+
const value = args[channelIdx + 1];
|
|
1158
|
+
if (!value || value.startsWith('--')) {
|
|
1159
|
+
console.log(chalk.yellow(' ⚠ --channel needs a value (stable | beta). Using "stable".'));
|
|
1160
|
+
} else if (value !== 'stable' && value !== 'beta') {
|
|
1161
|
+
console.log(chalk.yellow(` ⚠ Unknown channel "${value}". Using "stable".`));
|
|
1162
|
+
} else {
|
|
1163
|
+
channel = value;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
if (checkOnly) {
|
|
1169
|
+
return runCheck(channel);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// Pass channel through to the provisioner. The provisioner does the
|
|
1173
|
+
// actual npm install; channel maps to the npm dist-tag suffix.
|
|
1126
1174
|
return runProvision({
|
|
1127
1175
|
mode: 'update',
|
|
1128
|
-
dryRun:
|
|
1129
|
-
force:
|
|
1130
|
-
yes:
|
|
1131
|
-
restart: !
|
|
1176
|
+
dryRun: args.includes('--dry-run'),
|
|
1177
|
+
force: args.includes('--force'),
|
|
1178
|
+
yes: args.includes('--yes') || args.includes('-y'),
|
|
1179
|
+
restart: !args.includes('--no-restart'),
|
|
1180
|
+
channel,
|
|
1132
1181
|
});
|
|
1133
1182
|
}
|
|
1134
1183
|
|
|
1184
|
+
/**
|
|
1185
|
+
* v4.4.14 — `bizar update --check`. Print the version matrix and (if
|
|
1186
|
+
* a newer version exists) the release notes between current and latest.
|
|
1187
|
+
* Does NOT touch the system. Exits non-zero when an update is available
|
|
1188
|
+
* so callers can use it in CI / pre-flight scripts.
|
|
1189
|
+
*/
|
|
1190
|
+
export async function runCheck(channel = 'stable') {
|
|
1191
|
+
console.log(chalk.bold.hex('#a855f7')('\n ᚦ BIZAR UPDATE — CHECK ᚦ\n'));
|
|
1192
|
+
console.log(chalk.dim(` Channel: ${channel}\n`));
|
|
1193
|
+
|
|
1194
|
+
const state = detectState();
|
|
1195
|
+
printVersionMatrix([
|
|
1196
|
+
{ label: 'opencode-ai', current: state.opencodeCli.version, latest: state.opencodeCli.latest },
|
|
1197
|
+
{ label: PKG_MAIN, current: state.pkgVersion, latest: state.pkgLatest },
|
|
1198
|
+
]);
|
|
1199
|
+
|
|
1200
|
+
const cur = state.pkgVersion;
|
|
1201
|
+
const lat = state.pkgLatest;
|
|
1202
|
+
if (cur && lat && cur !== lat) {
|
|
1203
|
+
console.log(chalk.dim(`\n Release notes (${cur} → ${lat}):`));
|
|
1204
|
+
const notes = fetchReleaseNotes(PKG_MAIN, cur, lat).catch((err) => {
|
|
1205
|
+
console.log(chalk.dim(` (could not fetch: ${err.message || err})`));
|
|
1206
|
+
return null;
|
|
1207
|
+
});
|
|
1208
|
+
const text = await notes;
|
|
1209
|
+
if (text) {
|
|
1210
|
+
// Trim to a sensible cap (first 30 lines) so the console stays
|
|
1211
|
+
// readable; the full notes are one `npm view` call away.
|
|
1212
|
+
const lines = text.split(/\r?\n/).slice(0, 30);
|
|
1213
|
+
for (const line of lines) console.log(` ${line}`);
|
|
1214
|
+
if (lines.length === 30) console.log(chalk.dim(' ... (truncated; run `npm view @polderlabs/bizar` for full)'));
|
|
1215
|
+
}
|
|
1216
|
+
console.log(chalk.yellow(`\n ⤵ Run \`bizar update\` to apply.\n`));
|
|
1217
|
+
// Non-zero exit so scripts can detect the update-available case.
|
|
1218
|
+
return { ok: true, updateAvailable: true, channel };
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
console.log(chalk.green('\n ✓ Already on the latest version.\n'));
|
|
1222
|
+
return { ok: true, updateAvailable: false, channel };
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Fetch the release notes between two versions from the npm registry.
|
|
1227
|
+
* Returns a plain-text summary (the registry's "description" field for
|
|
1228
|
+
* the latest version), or null on any failure. Never throws — failures
|
|
1229
|
+
* are surfaced to the caller as null so the check command stays useful
|
|
1230
|
+
* offline.
|
|
1231
|
+
*/
|
|
1232
|
+
async function fetchReleaseNotes(pkg, _from, _to) {
|
|
1233
|
+
// The npm registry does not expose a structured per-version release-
|
|
1234
|
+
// notes field. The best we can do is `npm view <pkg> description`
|
|
1235
|
+
// which is the package's README excerpt. We surface that as "notes".
|
|
1236
|
+
try {
|
|
1237
|
+
const { execFileSync } = await import('node:child_process');
|
|
1238
|
+
const out = execFileSync('npm', ['view', pkg, 'description'], {
|
|
1239
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1240
|
+
timeout: 10000,
|
|
1241
|
+
encoding: 'utf8',
|
|
1242
|
+
}).toString().trim();
|
|
1243
|
+
return out || null;
|
|
1244
|
+
} catch {
|
|
1245
|
+
return null;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1135
1249
|
/**
|
|
1136
1250
|
* CLI-flag-parsing entrypoint for `bizar install`. Mirrors `runUpdate`
|
|
1137
1251
|
* so `cli/install.mjs` can stay a thin shim too.
|