@polderlabs/bizar 4.8.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.
- package/bizar-dash/dist/assets/icons-CFqu2M-c.js +656 -0
- package/bizar-dash/dist/assets/icons-CFqu2M-c.js.map +1 -0
- package/bizar-dash/dist/assets/index-DmpSFPJY.js +9 -0
- package/bizar-dash/dist/assets/index-DmpSFPJY.js.map +1 -0
- package/bizar-dash/dist/assets/main-Dl8yY5_H.js +16 -0
- package/bizar-dash/dist/assets/main-Dl8yY5_H.js.map +1 -0
- package/bizar-dash/dist/assets/{main-DX_Jh8Wc.css → main-ZAfGKENE.css} +1 -1
- package/bizar-dash/dist/assets/markdown-DIquRulQ.js +29 -0
- package/bizar-dash/dist/assets/markdown-DIquRulQ.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-C2gysFOZ.js +2 -0
- package/bizar-dash/dist/assets/mobile-C2gysFOZ.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-DHXXbn1A.js +1 -0
- package/bizar-dash/dist/assets/{mobile-Chvf9u_B.js.map → mobile-DHXXbn1A.js.map} +1 -1
- package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js +40 -0
- package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js.map +1 -0
- package/bizar-dash/dist/index.html +6 -3
- package/bizar-dash/dist/mobile.html +5 -2
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/skills/eval/SKILL.md +237 -0
- package/bizar-dash/src/server/api.mjs +28 -0
- package/bizar-dash/src/server/auth.mjs +155 -1
- package/bizar-dash/src/server/eval-store.mjs +226 -0
- package/bizar-dash/src/server/eval.mjs +347 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +109 -0
- package/bizar-dash/src/server/memory-store.mjs +121 -0
- package/bizar-dash/src/server/ocr.mjs +55 -0
- package/bizar-dash/src/server/otel.mjs +133 -0
- package/bizar-dash/src/server/plugins/registry.mjs +363 -0
- package/bizar-dash/src/server/plugins/sandbox.mjs +655 -0
- package/bizar-dash/src/server/plugins/store.mjs +659 -0
- package/bizar-dash/src/server/routes/chat.mjs +246 -170
- package/bizar-dash/src/server/routes/clipboard.mjs +173 -0
- package/bizar-dash/src/server/routes/eval.mjs +147 -0
- package/bizar-dash/src/server/routes/memory.mjs +46 -0
- package/bizar-dash/src/server/routes/ocr.mjs +182 -0
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +82 -48
- package/bizar-dash/src/server/routes/plugins.mjs +220 -0
- package/bizar-dash/src/server/routes/users.mjs +84 -0
- package/bizar-dash/src/server/routes/voice.mjs +131 -0
- package/bizar-dash/src/server/routes/workspaces.mjs +204 -0
- package/bizar-dash/src/server/server.mjs +40 -0
- package/bizar-dash/src/server/voice-store.mjs +202 -0
- package/bizar-dash/src/server/voice-transcribe.mjs +72 -0
- package/bizar-dash/src/server/workspaces.mjs +626 -0
- package/bizar-dash/src/web/components/InviteDialog.tsx +205 -0
- package/bizar-dash/src/web/components/ScreenshotCapture.tsx +138 -0
- package/bizar-dash/src/web/components/ScreenshotOCR.tsx +42 -0
- package/bizar-dash/src/web/components/SettingsSearch.tsx +204 -89
- package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +182 -0
- package/bizar-dash/src/web/components/VoiceRecorder.tsx +104 -0
- package/bizar-dash/src/web/components/WorkspaceSelector.tsx +158 -0
- package/bizar-dash/src/web/lib/search.ts +115 -0
- package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +10 -35
- package/bizar-dash/src/web/mobile/views/QrCodePanel.tsx +69 -0
- package/bizar-dash/src/web/styles/memory.css +166 -1
- package/bizar-dash/src/web/styles/settings.css +80 -0
- package/bizar-dash/src/web/views/Memory.tsx +22 -2
- package/bizar-dash/src/web/views/Settings.tsx +99 -0
- package/bizar-dash/src/web/views/Workspace.tsx +294 -0
- package/bizar-dash/src/web/views/memory/FromScreenshotPanel.tsx +23 -0
- package/bizar-dash/src/web/views/memory/MemoryGraphLegend.tsx +29 -0
- package/bizar-dash/src/web/views/memory/MemoryGraphPanel.tsx +192 -0
- package/bizar-dash/src/web/views/memory/MemoryGraphView.tsx +336 -0
- package/bizar-dash/src/web/views/memory/VaultFromClipboardPanel.tsx +101 -0
- package/bizar-dash/src/web/views/settings/WorkspacesSection.tsx +189 -0
- package/bizar-dash/tests/bundle-analysis.test.mjs +73 -0
- package/bizar-dash/tests/clipboard.test.mjs +147 -0
- package/bizar-dash/tests/components/screenshot-ocr.test.tsx +75 -0
- package/bizar-dash/tests/components/settings-search.test.tsx +180 -0
- package/bizar-dash/tests/components/workspace-selector.test.tsx +73 -0
- package/bizar-dash/tests/deploy-templates.test.mjs +100 -0
- package/bizar-dash/tests/docker-build.test.mjs +96 -0
- package/bizar-dash/tests/eval/fixtures.test.mjs +141 -0
- package/bizar-dash/tests/eval/report.test.mjs +284 -0
- package/bizar-dash/tests/eval/runner.test.mjs +471 -0
- package/bizar-dash/tests/lib/search-fuzzy.test.ts +149 -0
- package/bizar-dash/tests/memory-graph-view.test.tsx +69 -0
- package/bizar-dash/tests/memory-graph.test.mjs +95 -0
- package/bizar-dash/tests/ocr.test.mjs +87 -0
- package/bizar-dash/tests/otel.test.mjs +188 -0
- package/bizar-dash/tests/plugins-registry.test.mjs +387 -0
- package/bizar-dash/tests/plugins-sandbox.test.mjs +374 -0
- package/bizar-dash/tests/plugins-store.test.mjs +455 -0
- package/bizar-dash/tests/users.test.mjs +108 -0
- package/bizar-dash/tests/voice-recorder.test.tsx +95 -0
- package/bizar-dash/tests/voice-store.test.mjs +148 -0
- package/bizar-dash/tests/voice-transcribe.test.mjs +87 -0
- package/bizar-dash/tests/workspaces.test.mjs +527 -0
- package/cli/bin.mjs +72 -2
- package/cli/commands/clip.mjs +146 -0
- package/cli/commands/dash.mjs +6 -0
- package/cli/commands/deploy/cloudflare.mjs +250 -0
- package/cli/commands/deploy/docker.mjs +221 -0
- package/cli/commands/deploy/fly.mjs +161 -0
- package/cli/commands/deploy/vercel.mjs +225 -0
- package/cli/commands/deploy.mjs +240 -0
- package/cli/commands/eval.mjs +378 -0
- package/cli/commands/marketplace.mjs +64 -0
- package/cli/commands/ocr.mjs +165 -0
- package/cli/commands/plugin.mjs +358 -0
- package/cli/commands/voice.mjs +211 -0
- package/cli/commands/workspace.mjs +247 -0
- package/package.json +12 -2
- package/templates/deploy/cloudflare/README.md +32 -0
- package/templates/deploy/cloudflare/functions-index.template.js +15 -0
- package/templates/deploy/cloudflare/wrangler.toml.template +9 -0
- package/templates/deploy/docker/.env.template +16 -0
- package/templates/deploy/docker/README.md +58 -0
- package/templates/deploy/docker/docker-compose.template.yml +23 -0
- package/templates/deploy/fly/README.md +35 -0
- package/templates/deploy/fly/fly.toml.template +28 -0
- package/templates/deploy/vercel/README.md +29 -0
- package/templates/deploy/vercel/api-index.template.js +18 -0
- package/templates/deploy/vercel/vercel.json.template +16 -0
- package/templates/eval-fixtures/README.md +58 -0
- package/templates/eval-fixtures/code-search-basic.json +28 -0
- package/templates/eval-fixtures/latency-bounds.json +16 -0
- package/templates/eval-fixtures/regression-suite.json +79 -0
- package/templates/eval-fixtures/response-format.json +30 -0
- package/templates/eval-fixtures/tool-call-correctness.json +24 -0
- package/templates/plugin-template/README.md +121 -0
- package/templates/plugin-template/index.js +66 -0
- package/templates/plugin-template/plugin.json +42 -0
- package/templates/plugin-template/tests/plugin.test.js +83 -0
- package/bizar-dash/dist/assets/main-DHZmbnxQ.js +0 -361
- package/bizar-dash/dist/assets/main-DHZmbnxQ.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-BK8-ythT.js +0 -351
- package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-Chvf9u_B.js +0 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/clip.mjs
|
|
3
|
+
*
|
|
4
|
+
* v5.0.0 — Web Clipper CLI subcommand.
|
|
5
|
+
*
|
|
6
|
+
* Subcommands:
|
|
7
|
+
* bizar clip list List recent clips
|
|
8
|
+
* bizar clip delete <id> Delete a clip from the log
|
|
9
|
+
* bizar clip configure Show current dashboard URL
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { readDashboardConn } from './headroom.mjs';
|
|
14
|
+
|
|
15
|
+
// ── API helpers ────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
async function apiGet(path) {
|
|
18
|
+
const { port, secret } = readDashboardConn();
|
|
19
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
20
|
+
const url = `${baseUrl}${path}`;
|
|
21
|
+
const headers = { accept: 'application/json' };
|
|
22
|
+
if (secret) headers.authorization = `Basic ${Buffer.from(`opencode:${secret}`).toString('base64')}`;
|
|
23
|
+
const res = await fetch(url, { method: 'GET', headers });
|
|
24
|
+
const text = await res.text();
|
|
25
|
+
let data = null;
|
|
26
|
+
try { data = text ? JSON.parse(text) : null; } catch { /* ignore */ }
|
|
27
|
+
if (!res.ok) throw new Error(`${path}: ${data?.message || data?.error || res.statusText}`);
|
|
28
|
+
return data;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function apiDel(path) {
|
|
32
|
+
const { port, secret } = readDashboardConn();
|
|
33
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
34
|
+
const url = `${baseUrl}${path}`;
|
|
35
|
+
const headers = { accept: 'application/json' };
|
|
36
|
+
if (secret) headers.authorization = `Basic ${Buffer.from(`opencode:${secret}`).toString('base64')}`;
|
|
37
|
+
const res = await fetch(url, { method: 'DELETE', headers });
|
|
38
|
+
const text = await res.text();
|
|
39
|
+
let data = null;
|
|
40
|
+
try { data = text ? JSON.parse(text) : null; } catch { /* ignore */ }
|
|
41
|
+
if (!res.ok) throw new Error(`${path}: ${data?.message || data?.error || res.statusText}`);
|
|
42
|
+
return data;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Help ──────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export function showClipHelp() {
|
|
48
|
+
console.log(`
|
|
49
|
+
bizar clip — Manage web clips (saved via the browser extension or bookmarklet)
|
|
50
|
+
|
|
51
|
+
Usage:
|
|
52
|
+
bizar clip list List recent clips
|
|
53
|
+
bizar clip delete <id> Delete a clip from the log
|
|
54
|
+
bizar clip configure Show the current dashboard connection
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
bizar clip list
|
|
58
|
+
bizar clip delete my_page_1712345678
|
|
59
|
+
`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Subcommand handlers ────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
async function listClips() {
|
|
65
|
+
try {
|
|
66
|
+
const data = await apiGet('/api/clipboard/list');
|
|
67
|
+
const clips = data.clips || [];
|
|
68
|
+
if (clips.length === 0) {
|
|
69
|
+
console.log(chalk.dim(' No clips yet.'));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.log('');
|
|
73
|
+
console.log(chalk.bold(' Recent Clips'));
|
|
74
|
+
console.log('');
|
|
75
|
+
for (const c of clips) {
|
|
76
|
+
const date = c.savedAt ? new Date(c.savedAt).toLocaleString() : '—';
|
|
77
|
+
console.log(` ${chalk.bold(c.id)}`);
|
|
78
|
+
console.log(` Title: ${c.title || 'Untitled'}`);
|
|
79
|
+
console.log(` Saved: ${date}`);
|
|
80
|
+
console.log(` Path: ${c.notePath || '—'}`);
|
|
81
|
+
console.log('');
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error(chalk.red(` ✗ Failed to list clips: ${err.message}`));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function deleteClip(id) {
|
|
90
|
+
if (!id) {
|
|
91
|
+
console.error(chalk.red(' Error: clip ID is required'));
|
|
92
|
+
console.error(' Usage: bizar clip delete <id>');
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
await apiDel(`/api/clipboard/${encodeURIComponent(id)}`);
|
|
97
|
+
console.log(chalk.green(` ✓ Clip "${id}" deleted from log.`));
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (err.message.includes('404')) {
|
|
100
|
+
console.error(chalk.red(` ✗ Clip "${id}" not found.`));
|
|
101
|
+
} else {
|
|
102
|
+
console.error(chalk.red(` ✗ Failed to delete clip: ${err.message}`));
|
|
103
|
+
}
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function showConfigure() {
|
|
109
|
+
const conn = readDashboardConn();
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(chalk.bold(' Dashboard Connection'));
|
|
112
|
+
console.log('');
|
|
113
|
+
console.log(` URL: http://127.0.0.1:${conn.port}`);
|
|
114
|
+
console.log(` Auth: ${conn.secret ? 'enabled' : 'disabled'}`);
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log(chalk.dim(' To change the dashboard URL, use: bizar dash configure'));
|
|
117
|
+
console.log('');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Main dispatcher ────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
export async function run(name, args, isHelpRequest) {
|
|
123
|
+
const sub = args[0];
|
|
124
|
+
const subArgs = args.slice(1);
|
|
125
|
+
|
|
126
|
+
if (!sub || sub === '--help' || sub === '-h' || isHelpRequest) {
|
|
127
|
+
showClipHelp();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
switch (sub) {
|
|
132
|
+
case 'list':
|
|
133
|
+
await listClips();
|
|
134
|
+
break;
|
|
135
|
+
case 'delete':
|
|
136
|
+
await deleteClip(subArgs[0]);
|
|
137
|
+
break;
|
|
138
|
+
case 'configure':
|
|
139
|
+
showConfigure();
|
|
140
|
+
break;
|
|
141
|
+
default:
|
|
142
|
+
console.error(chalk.red(` ✗ Unknown subcommand: ${sub}`));
|
|
143
|
+
showClipHelp();
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
}
|
package/cli/commands/dash.mjs
CHANGED
|
@@ -42,6 +42,12 @@ export function showDashHelp() {
|
|
|
42
42
|
Note:
|
|
43
43
|
\`bizar dashboard\` is a deprecated alias for \`bizar dash\` and still
|
|
44
44
|
works, but new code should use \`bizar dash\`.
|
|
45
|
+
|
|
46
|
+
OpenTelemetry (optional, env vars):
|
|
47
|
+
BIZAR_OTEL=1 Enable OpenTelemetry tracing (off by default)
|
|
48
|
+
OTEL_ENABLED=1 Alias that also opts in
|
|
49
|
+
OTEL_EXPORTER_OTLP_ENDPOINT OTLP HTTP traces endpoint
|
|
50
|
+
(default http://localhost:4318/v1/traces)
|
|
45
51
|
`);
|
|
46
52
|
}
|
|
47
53
|
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/deploy/cloudflare.mjs
|
|
3
|
+
*
|
|
4
|
+
* Cloudflare Pages + Workers deployment via REST API.
|
|
5
|
+
*
|
|
6
|
+
* Generates wrangler.toml + functions/ (Workers entry) + public/ (static),
|
|
7
|
+
* then uploads to Cloudflare Pages API or runs wrangler CLI.
|
|
8
|
+
*/
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { execSync } from 'node:child_process';
|
|
13
|
+
|
|
14
|
+
const API_BASE = 'https://api.cloudflare.com/client/v4';
|
|
15
|
+
|
|
16
|
+
// ── Scaffold generation ──────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function generateScaffold(outDir, opts) {
|
|
19
|
+
const scaffoldRoot = join(outDir, 'cloudflare');
|
|
20
|
+
|
|
21
|
+
if (!existsSync(scaffoldRoot)) {
|
|
22
|
+
mkdirSync(scaffoldRoot, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// wrangler.toml
|
|
26
|
+
const wranglerContent = [
|
|
27
|
+
'name = "' + (opts.projectName || 'bizar-dash') + '"',
|
|
28
|
+
'main = "functions/index.js"',
|
|
29
|
+
'compatibility_date = "2025-01-01"',
|
|
30
|
+
'',
|
|
31
|
+
'[site]',
|
|
32
|
+
' bucket = "./public"',
|
|
33
|
+
'',
|
|
34
|
+
'[env.production]',
|
|
35
|
+
' routes = ["' + (opts.projectName || 'bizar-dash') + '.*.pages.dev/*"]',
|
|
36
|
+
'',
|
|
37
|
+
].join('\n');
|
|
38
|
+
writeFileSync(join(scaffoldRoot, 'wrangler.toml'), wranglerContent);
|
|
39
|
+
|
|
40
|
+
// functions/index.js — Workers function entry
|
|
41
|
+
const fnDir = join(scaffoldRoot, 'functions');
|
|
42
|
+
if (!existsSync(fnDir)) mkdirSync(fnDir, { recursive: true });
|
|
43
|
+
|
|
44
|
+
const functionsIndex = [
|
|
45
|
+
'// Cloudflare Workers entry — Bizar dashboard',
|
|
46
|
+
'// Generated by `bizar deploy --to cloudflare`',
|
|
47
|
+
'',
|
|
48
|
+
'export async function onRequest(context) {',
|
|
49
|
+
' const { request, env } = context;',
|
|
50
|
+
' const url = new URL(request.url);',
|
|
51
|
+
'',
|
|
52
|
+
' // Serve static assets from Pages KV',
|
|
53
|
+
' if (url.pathname.startsWith("/assets/")) {',
|
|
54
|
+
' return env.ASSETS.fetch(request);',
|
|
55
|
+
' }',
|
|
56
|
+
'',
|
|
57
|
+
' // SPA fallback',
|
|
58
|
+
' return env.ASSETS.fetch(new Request(url.origin + "/index.html", request));',
|
|
59
|
+
'}',
|
|
60
|
+
'',
|
|
61
|
+
].join('\n');
|
|
62
|
+
writeFileSync(join(fnDir, 'index.js'), functionsIndex);
|
|
63
|
+
|
|
64
|
+
// public/ — placeholder for dashboard build output
|
|
65
|
+
const publicDir = join(scaffoldRoot, 'public');
|
|
66
|
+
if (!existsSync(publicDir)) mkdirSync(publicDir, { recursive: true });
|
|
67
|
+
|
|
68
|
+
// Build the dashboard
|
|
69
|
+
console.log(chalk.dim(' Building dashboard bundle\u2026'));
|
|
70
|
+
try {
|
|
71
|
+
execSync('npm run build', {
|
|
72
|
+
cwd: opts.projectRoot,
|
|
73
|
+
encoding: 'utf8',
|
|
74
|
+
stdio: 'pipe',
|
|
75
|
+
timeout: 120000,
|
|
76
|
+
});
|
|
77
|
+
} catch (err) {
|
|
78
|
+
throw new Error('Dashboard build failed: ' + err.message);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Copy built assets into public/
|
|
82
|
+
const distDir = join(opts.projectRoot, 'dist');
|
|
83
|
+
if (existsSync(distDir)) {
|
|
84
|
+
execSync('cp -r "' + distDir + '"/* "' + publicDir + '/"', { stdio: 'pipe' });
|
|
85
|
+
} else {
|
|
86
|
+
console.log(chalk.yellow(' \u26a0 No dist/ found \u2014 deploying scaffold only'));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(chalk.dim(' \u2713 Scaffold generated at', scaffoldRoot));
|
|
90
|
+
return scaffoldRoot;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── API helpers ──────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
async function cfPost(path, token, body) {
|
|
96
|
+
const url = API_BASE + path;
|
|
97
|
+
const res = await fetch(url, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: {
|
|
100
|
+
Authorization: 'Bearer ' + token,
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify(body),
|
|
104
|
+
});
|
|
105
|
+
const json = await res.json();
|
|
106
|
+
if (!json.success) {
|
|
107
|
+
const msgs = (json.errors || []).map((e) => e.message).join('; ');
|
|
108
|
+
throw new Error('Cloudflare API error: ' + msgs || 'HTTP ' + res.status);
|
|
109
|
+
}
|
|
110
|
+
return json.result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function cfGet(path, token) {
|
|
114
|
+
const url = API_BASE + path;
|
|
115
|
+
const res = await fetch(url, {
|
|
116
|
+
headers: { Authorization: 'Bearer ' + token },
|
|
117
|
+
});
|
|
118
|
+
const json = await res.json();
|
|
119
|
+
if (!json.success) {
|
|
120
|
+
const msgs = (json.errors || []).map((e) => e.message).join('; ');
|
|
121
|
+
throw new Error('Cloudflare API error: ' + msgs || 'HTTP ' + res.status);
|
|
122
|
+
}
|
|
123
|
+
return json.result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Deployment ───────────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
export async function deployToCloudflare({ token, projectName, projectRoot, outDir }) {
|
|
129
|
+
if (!token) {
|
|
130
|
+
throw new Error('Cloudflare token required. Pass --token or set CLOUDFLARE_API_TOKEN env var.');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 1. Generate scaffold
|
|
134
|
+
const scaffoldRoot = generateScaffold(outDir, { projectName, projectRoot });
|
|
135
|
+
const name = projectName || 'bizar-dash';
|
|
136
|
+
|
|
137
|
+
// 2. Try wrangler CLI first (most reliable)
|
|
138
|
+
const hasWrangler = await checkWrangler();
|
|
139
|
+
if (hasWrangler) {
|
|
140
|
+
console.log(chalk.dim(' Deploying with wrangler CLI\u2026'));
|
|
141
|
+
try {
|
|
142
|
+
const output = execSync('npx wrangler pages deploy "' + scaffoldRoot + '" --project-name "' + name + '"' + (token ? ' --api-token ' + token : ''), {
|
|
143
|
+
cwd: scaffoldRoot,
|
|
144
|
+
encoding: 'utf8',
|
|
145
|
+
stdio: 'pipe',
|
|
146
|
+
timeout: 120000,
|
|
147
|
+
});
|
|
148
|
+
const urlMatch = output.match(/https:\/\/[^\s]+\.pages\.dev/);
|
|
149
|
+
const url = urlMatch ? urlMatch[0] : null;
|
|
150
|
+
if (url) {
|
|
151
|
+
return {
|
|
152
|
+
url,
|
|
153
|
+
deploymentId: name,
|
|
154
|
+
notes: ['Deployed via wrangler CLI.'],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
url: 'https://' + name + '.pages.dev',
|
|
159
|
+
deploymentId: name,
|
|
160
|
+
notes: ['Deployment complete. Check wrangler output for details.'],
|
|
161
|
+
};
|
|
162
|
+
} catch (err) {
|
|
163
|
+
throw new Error('wrangler deploy failed: ' + err.message);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 3. Fallback: use Cloudflare Pages API directly
|
|
168
|
+
console.log(chalk.dim(' Deploying via Cloudflare API\u2026'));
|
|
169
|
+
|
|
170
|
+
// List accounts to find the first one
|
|
171
|
+
const accounts = await cfGet('/accounts', token);
|
|
172
|
+
if (!accounts || accounts.length === 0) {
|
|
173
|
+
throw new Error('No Cloudflare account found');
|
|
174
|
+
}
|
|
175
|
+
const accountId = accounts[0].id;
|
|
176
|
+
|
|
177
|
+
// Create or get the Pages project
|
|
178
|
+
let project;
|
|
179
|
+
try {
|
|
180
|
+
project = await cfPost('/accounts/' + accountId + '/pages/projects', token, {
|
|
181
|
+
name,
|
|
182
|
+
production_branch: 'main',
|
|
183
|
+
});
|
|
184
|
+
} catch {
|
|
185
|
+
// Project may already exist — look it up
|
|
186
|
+
project = await cfGet('/accounts/' + accountId + '/pages/projects/' + name, token);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!project) {
|
|
190
|
+
throw new Error('Could not create or find Cloudflare Pages project: ' + name);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Upload files
|
|
194
|
+
const files = collectFiles(scaffoldRoot);
|
|
195
|
+
const uploadResult = await cfPost('/accounts/' + accountId + '/pages/projects/' + name + '/upload', token, {
|
|
196
|
+
files,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Start deployment
|
|
200
|
+
const deployment = await cfPost('/accounts/' + accountId + '/pages/projects/' + name + '/deployments', token, {
|
|
201
|
+
manifest: uploadResult.manifest || {},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const subdomain = project.subdomain || (name + '.pages.dev');
|
|
205
|
+
return {
|
|
206
|
+
url: 'https://' + subdomain,
|
|
207
|
+
deploymentId: deployment.id || name,
|
|
208
|
+
notes: [
|
|
209
|
+
'Cloudflare Pages project: ' + name,
|
|
210
|
+
'Manage at https://dash.cloudflare.com/?to=/:account/pages/view/' + name,
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
async function checkWrangler() {
|
|
218
|
+
try {
|
|
219
|
+
execSync('npx wrangler --version', {
|
|
220
|
+
encoding: 'utf8',
|
|
221
|
+
stdio: 'pipe',
|
|
222
|
+
timeout: 10000,
|
|
223
|
+
});
|
|
224
|
+
return true;
|
|
225
|
+
} catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function collectFiles(rootDir) {
|
|
231
|
+
const files = [];
|
|
232
|
+
|
|
233
|
+
function walk(dir, prefix) {
|
|
234
|
+
const entries = readdirSync(dir);
|
|
235
|
+
for (const entry of entries) {
|
|
236
|
+
const full = join(dir, entry);
|
|
237
|
+
const rel = prefix ? prefix + '/' + entry : entry;
|
|
238
|
+
const stat = statSync(full);
|
|
239
|
+
if (stat.isDirectory()) {
|
|
240
|
+
walk(full, rel);
|
|
241
|
+
} else {
|
|
242
|
+
const content = readFileSync(full, 'base64');
|
|
243
|
+
files.push({ file: rel, data: content, encoding: 'base64' });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
walk(rootDir, '');
|
|
249
|
+
return files;
|
|
250
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/deploy/docker.mjs
|
|
3
|
+
*
|
|
4
|
+
* Docker registry push deployment.
|
|
5
|
+
*
|
|
6
|
+
* Builds image: `docker build -t <registry>/<name>:<version> .`
|
|
7
|
+
* Pushes: `docker push <registry>/<name>:<version>`
|
|
8
|
+
* Optionally generates docker-compose.yml + .env for self-host.
|
|
9
|
+
*/
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { execSync } from 'node:child_process';
|
|
14
|
+
|
|
15
|
+
// ── Scaffold generation ──────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function generateComposeScaffold(outDir, opts) {
|
|
18
|
+
const scaffoldRoot = join(outDir, 'docker');
|
|
19
|
+
|
|
20
|
+
if (!existsSync(scaffoldRoot)) {
|
|
21
|
+
mkdirSync(scaffoldRoot, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// docker-compose.yml
|
|
25
|
+
const imageRef = (opts.registry || 'ghcr.io') + '/' + (opts.imageName || 'bizar-dash');
|
|
26
|
+
|
|
27
|
+
const composeYml = [
|
|
28
|
+
'# docker-compose.yml — Bizar dashboard',
|
|
29
|
+
'# Generated by `bizar deploy --to docker --compose`',
|
|
30
|
+
'',
|
|
31
|
+
'version: "3.8"',
|
|
32
|
+
'',
|
|
33
|
+
'services:',
|
|
34
|
+
' bizar-dash:',
|
|
35
|
+
' image: ' + imageRef + ':latest',
|
|
36
|
+
' container_name: bizar-dash',
|
|
37
|
+
' restart: unless-stopped',
|
|
38
|
+
' ports:',
|
|
39
|
+
' - "4097:4097"',
|
|
40
|
+
' - "4098:4098"',
|
|
41
|
+
' environment:',
|
|
42
|
+
' - BIZAR_DASHBOARD_PORT=4097',
|
|
43
|
+
' - BIZAR_DASHBOARD_HOST=0.0.0.0',
|
|
44
|
+
' - NODE_ENV=production',
|
|
45
|
+
' env_file:',
|
|
46
|
+
' - .env',
|
|
47
|
+
' volumes:',
|
|
48
|
+
' - bizar-config:/home/bizar/.config/bizar',
|
|
49
|
+
' - bizar-memory:/home/bizar/.local/share/bizar/memory',
|
|
50
|
+
'',
|
|
51
|
+
'volumes:',
|
|
52
|
+
' bizar-config:',
|
|
53
|
+
' bizar-memory:',
|
|
54
|
+
'',
|
|
55
|
+
].join('\n');
|
|
56
|
+
writeFileSync(join(scaffoldRoot, 'docker-compose.yml'), composeYml);
|
|
57
|
+
|
|
58
|
+
// .env template
|
|
59
|
+
const envTemplate = [
|
|
60
|
+
'# .env — Bizar dashboard environment variables',
|
|
61
|
+
'# Generated by `bizar deploy --to docker --compose`',
|
|
62
|
+
'#',
|
|
63
|
+
'# Copy this file to .env and fill in your values.',
|
|
64
|
+
'',
|
|
65
|
+
'# Dashboard',
|
|
66
|
+
'# BIZAR_DASHBOARD_PORT=4097',
|
|
67
|
+
'# BIZAR_DASHBOARD_HOST=0.0.0.0',
|
|
68
|
+
'',
|
|
69
|
+
'# OpenTelemetry (optional)',
|
|
70
|
+
'# BIZAR_OTEL=1',
|
|
71
|
+
'# OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318',
|
|
72
|
+
'',
|
|
73
|
+
'# Tailscale (optional)',
|
|
74
|
+
'# TAILSCALE_AUTHKEY=tskey-auth-xxxxxxxxxxxxxxxxxxxx',
|
|
75
|
+
'',
|
|
76
|
+
'# Headroom context compression (optional)',
|
|
77
|
+
'# HEADROOM_ENABLED=1',
|
|
78
|
+
'',
|
|
79
|
+
].join('\n');
|
|
80
|
+
writeFileSync(join(scaffoldRoot, '.env.template'), envTemplate);
|
|
81
|
+
|
|
82
|
+
console.log(chalk.dim(' \u2713 Docker Compose scaffold generated at', scaffoldRoot));
|
|
83
|
+
return scaffoldRoot;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Docker daemon check ──────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
function checkDocker() {
|
|
89
|
+
try {
|
|
90
|
+
execSync('docker info', {
|
|
91
|
+
encoding: 'utf8',
|
|
92
|
+
stdio: 'pipe',
|
|
93
|
+
timeout: 5000,
|
|
94
|
+
});
|
|
95
|
+
return true;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Deployment ───────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
export async function deployToDocker({ token, registry, imageName, projectRoot, outDir, compose }) {
|
|
104
|
+
// Handle --compose only mode
|
|
105
|
+
if (compose) {
|
|
106
|
+
const scaffoldRoot = generateComposeScaffold(outDir, { registry, imageName });
|
|
107
|
+
return {
|
|
108
|
+
url: null,
|
|
109
|
+
deploymentId: null,
|
|
110
|
+
notes: [
|
|
111
|
+
'Docker Compose files generated at ' + scaffoldRoot,
|
|
112
|
+
'To run: cd ' + scaffoldRoot + ' && docker compose up -d',
|
|
113
|
+
'To customize, edit docker-compose.yml and .env',
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Full build + push
|
|
119
|
+
if (!checkDocker()) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
'Docker daemon is not running. Start Docker and try again.\n' +
|
|
122
|
+
' systemctl start docker (Linux)\n' +
|
|
123
|
+
' open -a Docker (macOS)',
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const reg = registry || 'ghcr.io';
|
|
128
|
+
const imgName = imageName || 'bizar-dash';
|
|
129
|
+
const version = getPackageVersion(projectRoot);
|
|
130
|
+
const imageRef = reg + '/' + imgName;
|
|
131
|
+
const tag = imageRef + ':' + version;
|
|
132
|
+
const latestTag = imageRef + ':latest';
|
|
133
|
+
|
|
134
|
+
// 1. Build the image
|
|
135
|
+
console.log(chalk.dim(' Building Docker image\u2026'));
|
|
136
|
+
try {
|
|
137
|
+
execSync('docker build -t "' + tag + '" -t "' + latestTag + '" .', {
|
|
138
|
+
cwd: projectRoot,
|
|
139
|
+
encoding: 'utf8',
|
|
140
|
+
stdio: 'pipe',
|
|
141
|
+
timeout: 300000, // 5 minutes
|
|
142
|
+
});
|
|
143
|
+
} catch (err) {
|
|
144
|
+
throw new Error('Docker build failed: ' + err.message);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 2. Login if token provided
|
|
148
|
+
if (token) {
|
|
149
|
+
console.log(chalk.dim(' Authenticating with registry\u2026'));
|
|
150
|
+
const loginCmd = 'docker login ' + reg + ' --username=oauth --password-stdin';
|
|
151
|
+
try {
|
|
152
|
+
execSync(loginCmd, {
|
|
153
|
+
input: token,
|
|
154
|
+
encoding: 'utf8',
|
|
155
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
156
|
+
timeout: 15000,
|
|
157
|
+
});
|
|
158
|
+
} catch (err) {
|
|
159
|
+
throw new Error('Docker login failed: ' + err.message);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 3. Push the image
|
|
164
|
+
console.log(chalk.dim(' Pushing image to ' + reg + '\u2026'));
|
|
165
|
+
try {
|
|
166
|
+
execSync('docker push "' + tag + '"', {
|
|
167
|
+
encoding: 'utf8',
|
|
168
|
+
stdio: 'pipe',
|
|
169
|
+
timeout: 300000,
|
|
170
|
+
});
|
|
171
|
+
execSync('docker push "' + latestTag + '"', {
|
|
172
|
+
encoding: 'utf8',
|
|
173
|
+
stdio: 'pipe',
|
|
174
|
+
timeout: 300000,
|
|
175
|
+
});
|
|
176
|
+
} catch (err) {
|
|
177
|
+
throw new Error('Docker push failed: ' + err.message);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const digest = getDigest(imageRef);
|
|
181
|
+
return {
|
|
182
|
+
url: null,
|
|
183
|
+
deploymentId: tag,
|
|
184
|
+
notes: [
|
|
185
|
+
'Image: ' + tag,
|
|
186
|
+
'Latest: ' + latestTag,
|
|
187
|
+
(digest ? 'Digest: ' + digest : ''),
|
|
188
|
+
'',
|
|
189
|
+
'To pull: docker pull ' + tag,
|
|
190
|
+
'To run: docker run -p 4097:4097 ' + tag,
|
|
191
|
+
'To compose: bizar deploy --to docker --compose',
|
|
192
|
+
].filter(Boolean),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
function getPackageVersion(projectRoot) {
|
|
199
|
+
try {
|
|
200
|
+
const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
|
|
201
|
+
return pkg.version || 'latest';
|
|
202
|
+
} catch {
|
|
203
|
+
return 'latest';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function getDigest(imageRef) {
|
|
208
|
+
try {
|
|
209
|
+
const output = execSync('docker inspect --format="{{.RepoDigests}}" "' + imageRef + '"', {
|
|
210
|
+
encoding: 'utf8',
|
|
211
|
+
stdio: 'pipe',
|
|
212
|
+
timeout: 5000,
|
|
213
|
+
});
|
|
214
|
+
const match = output.match(/sha256:[a-f0-9]{64}/);
|
|
215
|
+
return match ? match[0] : null;
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|