@bonnard/cli 0.2.15 → 0.3.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/README.md +9 -2
- package/dist/bin/bon.mjs +384 -32
- package/dist/docs/topics/dashboards.md +21 -24
- package/dist/docs/topics/sdk.apexcharts.md +281 -0
- package/dist/docs/topics/sdk.authentication.md +130 -0
- package/dist/docs/topics/sdk.browser.md +181 -0
- package/dist/docs/topics/sdk.chartjs.md +327 -0
- package/dist/docs/topics/sdk.echarts.md +297 -0
- package/dist/docs/topics/sdk.md +95 -0
- package/dist/docs/topics/sdk.query-reference.md +307 -0
- package/dist/templates/claude/skills/bonnard-build-dashboard/SKILL.md +145 -0
- package/dist/templates/claude/skills/bonnard-get-started/SKILL.md +1 -1
- package/dist/templates/claude/skills/bonnard-metabase-migrate/SKILL.md +1 -1
- package/dist/templates/cursor/rules/bonnard-build-dashboard.mdc +144 -0
- package/dist/templates/cursor/rules/bonnard-get-started.mdc +1 -1
- package/dist/templates/cursor/rules/bonnard-metabase-migrate.mdc +1 -1
- package/dist/templates/shared/bonnard.md +7 -1
- package/dist/viewer.html +261 -0
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ Agent support:
|
|
|
98
98
|
Set up your MCP server so agents can query your semantic layer directly:
|
|
99
99
|
|
|
100
100
|
```bash
|
|
101
|
-
bon mcp
|
|
101
|
+
bon mcp # Show MCP server setup instructions
|
|
102
102
|
bon mcp test # Verify the connection
|
|
103
103
|
```
|
|
104
104
|
|
|
@@ -172,8 +172,15 @@ Handles automatic datasource synchronisation and skips interactive prompts. Fits
|
|
|
172
172
|
| `bon diff` | Preview changes before deploying |
|
|
173
173
|
| `bon annotate` | Add metadata and descriptions to models |
|
|
174
174
|
| `bon query` | Run queries from the terminal (JSON or SQL) |
|
|
175
|
-
| `bon mcp
|
|
175
|
+
| `bon mcp` | Show MCP server setup instructions |
|
|
176
176
|
| `bon mcp test` | Test MCP connection |
|
|
177
|
+
| `bon dashboard dev` | Preview a markdown dashboard locally with live reload |
|
|
178
|
+
| `bon dashboard deploy` | Deploy a markdown or HTML dashboard |
|
|
179
|
+
| `bon dashboard list` | List deployed dashboards |
|
|
180
|
+
| `bon dashboard remove` | Remove a deployed dashboard |
|
|
181
|
+
| `bon dashboard open` | Open a dashboard in the browser |
|
|
182
|
+
| `bon pull` | Download deployed models to local project |
|
|
183
|
+
| `bon keys list` / `create` / `revoke` | Manage API keys |
|
|
177
184
|
| `bon docs` | Browse or search documentation from the CLI |
|
|
178
185
|
| `bon login` / `bon logout` | Manage authentication |
|
|
179
186
|
| `bon whoami` | Check current session |
|
package/dist/bin/bon.mjs
CHANGED
|
@@ -13,6 +13,8 @@ import YAML from "yaml";
|
|
|
13
13
|
import http from "node:http";
|
|
14
14
|
import crypto from "node:crypto";
|
|
15
15
|
import { encode } from "@toon-format/toon";
|
|
16
|
+
import { createInterface } from "node:readline";
|
|
17
|
+
import open from "open";
|
|
16
18
|
|
|
17
19
|
//#region \0rolldown/runtime.js
|
|
18
20
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
@@ -555,36 +557,120 @@ function loadJsonTemplate(relativePath) {
|
|
|
555
557
|
return JSON.parse(content);
|
|
556
558
|
}
|
|
557
559
|
/**
|
|
558
|
-
* Write a template file
|
|
560
|
+
* Write a template file.
|
|
561
|
+
* - init mode: skip if file contains `# Bonnard`, append if exists without it, create if missing
|
|
562
|
+
* - update mode: overwrite if changed, create if missing
|
|
559
563
|
*/
|
|
560
|
-
function writeTemplateFile(content, targetPath,
|
|
564
|
+
function writeTemplateFile(content, targetPath, results, mode = "init") {
|
|
565
|
+
const relativePath = path.relative(process.cwd(), targetPath);
|
|
561
566
|
if (fs.existsSync(targetPath)) {
|
|
562
|
-
|
|
567
|
+
const existingContent = fs.readFileSync(targetPath, "utf-8");
|
|
568
|
+
if (mode === "update") if (existingContent === content) results.push({
|
|
569
|
+
action: "unchanged",
|
|
570
|
+
path: relativePath
|
|
571
|
+
});
|
|
572
|
+
else {
|
|
573
|
+
fs.writeFileSync(targetPath, content);
|
|
574
|
+
results.push({
|
|
575
|
+
action: "updated",
|
|
576
|
+
path: relativePath
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
else if (!existingContent.includes("# Bonnard")) {
|
|
580
|
+
fs.appendFileSync(targetPath, `\n\n${content}`);
|
|
581
|
+
results.push({
|
|
582
|
+
action: "appended",
|
|
583
|
+
path: relativePath
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
fs.writeFileSync(targetPath, content);
|
|
588
|
+
results.push({
|
|
589
|
+
action: mode === "update" ? "added" : "created",
|
|
590
|
+
path: relativePath
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Write a file that may contain user content alongside the Bonnard section.
|
|
596
|
+
* In update mode, replaces only content from `# Bonnard Semantic Layer` onwards,
|
|
597
|
+
* preserving any user content before it. Falls back to overwrite if marker not found.
|
|
598
|
+
*/
|
|
599
|
+
function writeBonnardSection(content, targetPath, results, mode = "init") {
|
|
600
|
+
const relativePath = path.relative(process.cwd(), targetPath);
|
|
601
|
+
const SECTION_MARKER = "# Bonnard Semantic Layer";
|
|
602
|
+
if (fs.existsSync(targetPath)) {
|
|
603
|
+
const existingContent = fs.readFileSync(targetPath, "utf-8");
|
|
604
|
+
if (mode === "update") {
|
|
605
|
+
const sectionStart = existingContent.indexOf(SECTION_MARKER);
|
|
606
|
+
let newContent;
|
|
607
|
+
if (sectionStart > 0) newContent = existingContent.slice(0, sectionStart).trimEnd() + "\n\n" + content;
|
|
608
|
+
else newContent = content;
|
|
609
|
+
if (existingContent === newContent) results.push({
|
|
610
|
+
action: "unchanged",
|
|
611
|
+
path: relativePath
|
|
612
|
+
});
|
|
613
|
+
else {
|
|
614
|
+
fs.writeFileSync(targetPath, newContent);
|
|
615
|
+
results.push({
|
|
616
|
+
action: "updated",
|
|
617
|
+
path: relativePath
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
} else if (!existingContent.includes("# Bonnard")) {
|
|
563
621
|
fs.appendFileSync(targetPath, `\n\n${content}`);
|
|
564
|
-
|
|
622
|
+
results.push({
|
|
623
|
+
action: "appended",
|
|
624
|
+
path: relativePath
|
|
625
|
+
});
|
|
565
626
|
}
|
|
566
627
|
} else {
|
|
567
628
|
fs.writeFileSync(targetPath, content);
|
|
568
|
-
|
|
629
|
+
results.push({
|
|
630
|
+
action: mode === "update" ? "added" : "created",
|
|
631
|
+
path: relativePath
|
|
632
|
+
});
|
|
569
633
|
}
|
|
570
634
|
}
|
|
571
635
|
/**
|
|
572
636
|
* Merge settings.json, preserving existing settings
|
|
573
637
|
*/
|
|
574
|
-
function mergeSettingsJson(templateSettings, targetPath,
|
|
638
|
+
function mergeSettingsJson(templateSettings, targetPath, results, mode = "init") {
|
|
639
|
+
const relativePath = path.relative(process.cwd(), targetPath);
|
|
575
640
|
if (fs.existsSync(targetPath)) {
|
|
576
|
-
const
|
|
641
|
+
const existingRaw = fs.readFileSync(targetPath, "utf-8");
|
|
642
|
+
const existingContent = JSON.parse(existingRaw);
|
|
577
643
|
const templatePerms = templateSettings.permissions;
|
|
578
644
|
if (templatePerms?.allow) {
|
|
579
645
|
existingContent.permissions = existingContent.permissions || {};
|
|
580
646
|
existingContent.permissions.allow = existingContent.permissions.allow || [];
|
|
581
647
|
for (const permission of templatePerms.allow) if (!existingContent.permissions.allow.includes(permission)) existingContent.permissions.allow.push(permission);
|
|
582
648
|
}
|
|
583
|
-
|
|
584
|
-
|
|
649
|
+
const newRaw = JSON.stringify(existingContent, null, 2) + "\n";
|
|
650
|
+
if (mode === "update") if (existingRaw === newRaw) results.push({
|
|
651
|
+
action: "unchanged",
|
|
652
|
+
path: relativePath
|
|
653
|
+
});
|
|
654
|
+
else {
|
|
655
|
+
fs.writeFileSync(targetPath, newRaw);
|
|
656
|
+
results.push({
|
|
657
|
+
action: "updated",
|
|
658
|
+
path: relativePath
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
fs.writeFileSync(targetPath, newRaw);
|
|
663
|
+
results.push({
|
|
664
|
+
action: "merged",
|
|
665
|
+
path: relativePath
|
|
666
|
+
});
|
|
667
|
+
}
|
|
585
668
|
} else {
|
|
586
669
|
fs.writeFileSync(targetPath, JSON.stringify(templateSettings, null, 2) + "\n");
|
|
587
|
-
|
|
670
|
+
results.push({
|
|
671
|
+
action: mode === "update" ? "added" : "created",
|
|
672
|
+
path: relativePath
|
|
673
|
+
});
|
|
588
674
|
}
|
|
589
675
|
}
|
|
590
676
|
/**
|
|
@@ -601,8 +687,8 @@ alwaysApply: ${alwaysApply}
|
|
|
601
687
|
/**
|
|
602
688
|
* Create agent templates (Claude Code, Cursor, and Codex)
|
|
603
689
|
*/
|
|
604
|
-
function createAgentTemplates(cwd, env) {
|
|
605
|
-
const
|
|
690
|
+
function createAgentTemplates(cwd, env, mode = "init") {
|
|
691
|
+
const results = [];
|
|
606
692
|
let sharedBonnard = loadTemplate("shared/bonnard.md");
|
|
607
693
|
if (env) sharedBonnard += "\n\n" + generateProjectContext(env);
|
|
608
694
|
const claudeRulesDir = path.join(cwd, ".claude", "rules");
|
|
@@ -611,31 +697,65 @@ function createAgentTemplates(cwd, env) {
|
|
|
611
697
|
fs.mkdirSync(path.join(claudeSkillsDir, "bonnard-get-started"), { recursive: true });
|
|
612
698
|
fs.mkdirSync(path.join(claudeSkillsDir, "bonnard-metabase-migrate"), { recursive: true });
|
|
613
699
|
fs.mkdirSync(path.join(claudeSkillsDir, "bonnard-design-guide"), { recursive: true });
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
writeTemplateFile(loadTemplate("claude/skills/bonnard-
|
|
617
|
-
writeTemplateFile(loadTemplate("claude/skills/bonnard-
|
|
618
|
-
|
|
700
|
+
fs.mkdirSync(path.join(claudeSkillsDir, "bonnard-build-dashboard"), { recursive: true });
|
|
701
|
+
writeBonnardSection(sharedBonnard, path.join(claudeRulesDir, "bonnard.md"), results, mode);
|
|
702
|
+
writeTemplateFile(loadTemplate("claude/skills/bonnard-get-started/SKILL.md"), path.join(claudeSkillsDir, "bonnard-get-started", "SKILL.md"), results, mode);
|
|
703
|
+
writeTemplateFile(loadTemplate("claude/skills/bonnard-metabase-migrate/SKILL.md"), path.join(claudeSkillsDir, "bonnard-metabase-migrate", "SKILL.md"), results, mode);
|
|
704
|
+
writeTemplateFile(loadTemplate("claude/skills/bonnard-design-guide/SKILL.md"), path.join(claudeSkillsDir, "bonnard-design-guide", "SKILL.md"), results, mode);
|
|
705
|
+
writeTemplateFile(loadTemplate("claude/skills/bonnard-build-dashboard/SKILL.md"), path.join(claudeSkillsDir, "bonnard-build-dashboard", "SKILL.md"), results, mode);
|
|
706
|
+
mergeSettingsJson(loadJsonTemplate("claude/settings.json"), path.join(cwd, ".claude", "settings.json"), results, mode);
|
|
619
707
|
const cursorRulesDir = path.join(cwd, ".cursor", "rules");
|
|
620
708
|
fs.mkdirSync(cursorRulesDir, { recursive: true });
|
|
621
|
-
writeTemplateFile(withCursorFrontmatter(sharedBonnard, "Bonnard semantic layer project context", true), path.join(cursorRulesDir, "bonnard.mdc"),
|
|
622
|
-
writeTemplateFile(loadTemplate("cursor/rules/bonnard-get-started.mdc"), path.join(cursorRulesDir, "bonnard-get-started.mdc"),
|
|
623
|
-
writeTemplateFile(loadTemplate("cursor/rules/bonnard-metabase-migrate.mdc"), path.join(cursorRulesDir, "bonnard-metabase-migrate.mdc"),
|
|
624
|
-
writeTemplateFile(loadTemplate("cursor/rules/bonnard-design-guide.mdc"), path.join(cursorRulesDir, "bonnard-design-guide.mdc"),
|
|
709
|
+
writeTemplateFile(withCursorFrontmatter(sharedBonnard, "Bonnard semantic layer project context", true), path.join(cursorRulesDir, "bonnard.mdc"), results, mode);
|
|
710
|
+
writeTemplateFile(loadTemplate("cursor/rules/bonnard-get-started.mdc"), path.join(cursorRulesDir, "bonnard-get-started.mdc"), results, mode);
|
|
711
|
+
writeTemplateFile(loadTemplate("cursor/rules/bonnard-metabase-migrate.mdc"), path.join(cursorRulesDir, "bonnard-metabase-migrate.mdc"), results, mode);
|
|
712
|
+
writeTemplateFile(loadTemplate("cursor/rules/bonnard-design-guide.mdc"), path.join(cursorRulesDir, "bonnard-design-guide.mdc"), results, mode);
|
|
713
|
+
writeTemplateFile(loadTemplate("cursor/rules/bonnard-build-dashboard.mdc"), path.join(cursorRulesDir, "bonnard-build-dashboard.mdc"), results, mode);
|
|
625
714
|
const codexSkillsDir = path.join(cwd, ".agents", "skills");
|
|
626
715
|
fs.mkdirSync(path.join(codexSkillsDir, "bonnard-get-started"), { recursive: true });
|
|
627
716
|
fs.mkdirSync(path.join(codexSkillsDir, "bonnard-metabase-migrate"), { recursive: true });
|
|
628
717
|
fs.mkdirSync(path.join(codexSkillsDir, "bonnard-design-guide"), { recursive: true });
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
writeTemplateFile(loadTemplate("claude/skills/bonnard-
|
|
632
|
-
writeTemplateFile(loadTemplate("claude/skills/bonnard-
|
|
633
|
-
|
|
718
|
+
fs.mkdirSync(path.join(codexSkillsDir, "bonnard-build-dashboard"), { recursive: true });
|
|
719
|
+
writeBonnardSection(sharedBonnard, path.join(cwd, "AGENTS.md"), results, mode);
|
|
720
|
+
writeTemplateFile(loadTemplate("claude/skills/bonnard-get-started/SKILL.md"), path.join(codexSkillsDir, "bonnard-get-started", "SKILL.md"), results, mode);
|
|
721
|
+
writeTemplateFile(loadTemplate("claude/skills/bonnard-metabase-migrate/SKILL.md"), path.join(codexSkillsDir, "bonnard-metabase-migrate", "SKILL.md"), results, mode);
|
|
722
|
+
writeTemplateFile(loadTemplate("claude/skills/bonnard-design-guide/SKILL.md"), path.join(codexSkillsDir, "bonnard-design-guide", "SKILL.md"), results, mode);
|
|
723
|
+
writeTemplateFile(loadTemplate("claude/skills/bonnard-build-dashboard/SKILL.md"), path.join(codexSkillsDir, "bonnard-build-dashboard", "SKILL.md"), results, mode);
|
|
724
|
+
return results;
|
|
725
|
+
}
|
|
726
|
+
function formatFileResult(result) {
|
|
727
|
+
switch (result.action) {
|
|
728
|
+
case "appended": return `${result.path} (appended)`;
|
|
729
|
+
case "merged": return `${result.path} (merged)`;
|
|
730
|
+
default: return result.path;
|
|
731
|
+
}
|
|
634
732
|
}
|
|
635
|
-
async function initCommand() {
|
|
733
|
+
async function initCommand(options = {}) {
|
|
636
734
|
const cwd = process.cwd();
|
|
637
|
-
const projectName = path.basename(cwd);
|
|
638
735
|
const paths = getProjectPaths(cwd);
|
|
736
|
+
if (options.update) {
|
|
737
|
+
if (!fs.existsSync(paths.config)) {
|
|
738
|
+
console.log(pc.red("No bon.yaml found. Run `bon init` first."));
|
|
739
|
+
process.exit(1);
|
|
740
|
+
}
|
|
741
|
+
const env = detectProjectEnvironment(cwd);
|
|
742
|
+
const results = createAgentTemplates(cwd, env.tools.length > 0 || env.warehouse ? env : void 0, "update");
|
|
743
|
+
const updated = results.filter((r) => r.action === "updated");
|
|
744
|
+
const added = results.filter((r) => r.action === "added");
|
|
745
|
+
if (updated.length === 0 && added.length === 0) console.log(pc.green("All agent templates are up to date."));
|
|
746
|
+
else {
|
|
747
|
+
const parts = [];
|
|
748
|
+
if (updated.length > 0) parts.push(`updated ${updated.length} file${updated.length !== 1 ? "s" : ""}`);
|
|
749
|
+
if (added.length > 0) parts.push(`added ${added.length} new file${added.length !== 1 ? "s" : ""}`);
|
|
750
|
+
console.log(pc.green(`Agent templates: ${parts.join(", ")}.`));
|
|
751
|
+
for (const r of [...updated, ...added]) {
|
|
752
|
+
const label = r.action === "added" ? pc.cyan("new") : pc.yellow("updated");
|
|
753
|
+
console.log(` ${label} ${pc.dim(r.path)}`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
const projectName = path.basename(cwd);
|
|
639
759
|
if (fs.existsSync(paths.config)) {
|
|
640
760
|
console.log(pc.red("A bon.yaml already exists in this directory."));
|
|
641
761
|
process.exit(1);
|
|
@@ -658,7 +778,7 @@ async function initCommand() {
|
|
|
658
778
|
if (agentFiles.length > 0) {
|
|
659
779
|
console.log();
|
|
660
780
|
console.log(pc.bold("Agent support:"));
|
|
661
|
-
for (const file of agentFiles) console.log(` ${pc.dim(file)}`);
|
|
781
|
+
for (const file of agentFiles) console.log(` ${pc.dim(formatFileResult(file))}`);
|
|
662
782
|
}
|
|
663
783
|
if (env.tools.length > 0 || env.warehouse) {
|
|
664
784
|
console.log();
|
|
@@ -670,12 +790,12 @@ async function initCommand() {
|
|
|
670
790
|
|
|
671
791
|
//#endregion
|
|
672
792
|
//#region src/commands/login.ts
|
|
673
|
-
const APP_URL = process.env.BON_APP_URL || "https://app.bonnard.dev";
|
|
793
|
+
const APP_URL$4 = process.env.BON_APP_URL || "https://app.bonnard.dev";
|
|
674
794
|
const TIMEOUT_MS = 120 * 1e3;
|
|
675
795
|
async function loginCommand() {
|
|
676
796
|
const state = crypto.randomUUID();
|
|
677
797
|
const { port, close } = await startCallbackServer(state);
|
|
678
|
-
const url = `${APP_URL}/auth/device?state=${state}&port=${port}`;
|
|
798
|
+
const url = `${APP_URL$4}/auth/device?state=${state}&port=${port}`;
|
|
679
799
|
console.log(pc.dim(`Opening browser to ${url}`));
|
|
680
800
|
const open = (await import("open")).default;
|
|
681
801
|
await open(url);
|
|
@@ -3870,11 +3990,237 @@ async function keysRevokeCommand(nameOrPrefix) {
|
|
|
3870
3990
|
}
|
|
3871
3991
|
}
|
|
3872
3992
|
|
|
3993
|
+
//#endregion
|
|
3994
|
+
//#region src/commands/dashboard/deploy.ts
|
|
3995
|
+
const APP_URL$3 = process.env.BON_APP_URL || "https://app.bonnard.dev";
|
|
3996
|
+
/**
|
|
3997
|
+
* Extract <title> content from HTML string
|
|
3998
|
+
*/
|
|
3999
|
+
function extractHtmlTitle(html) {
|
|
4000
|
+
const match = html.match(/<title[^>]*>(.*?)<\/title>/is);
|
|
4001
|
+
return match ? match[1].trim() : null;
|
|
4002
|
+
}
|
|
4003
|
+
/**
|
|
4004
|
+
* Extract title from YAML frontmatter in markdown
|
|
4005
|
+
*/
|
|
4006
|
+
function extractFrontmatterTitle(content) {
|
|
4007
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
4008
|
+
if (!match) return null;
|
|
4009
|
+
const titleMatch = match[1].match(/^title:\s*["']?(.+?)["']?\s*$/m);
|
|
4010
|
+
return titleMatch ? titleMatch[1] : null;
|
|
4011
|
+
}
|
|
4012
|
+
/**
|
|
4013
|
+
* Derive slug from filename: strip extension, lowercase, replace non-alphanumeric with hyphens
|
|
4014
|
+
*/
|
|
4015
|
+
function slugFromFilename(filename) {
|
|
4016
|
+
return path.basename(filename, path.extname(filename)).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
4017
|
+
}
|
|
4018
|
+
async function dashboardDeployCommand(file, options) {
|
|
4019
|
+
const filePath = path.resolve(file);
|
|
4020
|
+
if (!fs.existsSync(filePath)) {
|
|
4021
|
+
console.error(pc.red(`File not found: ${file}`));
|
|
4022
|
+
process.exit(1);
|
|
4023
|
+
}
|
|
4024
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
4025
|
+
if (content.length > 2 * 1024 * 1024) {
|
|
4026
|
+
console.error(pc.red("File exceeds 2MB limit."));
|
|
4027
|
+
process.exit(1);
|
|
4028
|
+
}
|
|
4029
|
+
const format = path.extname(filePath).toLowerCase() === ".md" ? "markdown" : "html";
|
|
4030
|
+
const slug = options.slug || slugFromFilename(file);
|
|
4031
|
+
const title = options.title || (format === "markdown" ? extractFrontmatterTitle(content) || slug : extractHtmlTitle(content) || slug);
|
|
4032
|
+
console.log(pc.dim(`Deploying ${format} dashboard "${slug}"...`));
|
|
4033
|
+
try {
|
|
4034
|
+
const dashboard = (await post("/api/dashboards", {
|
|
4035
|
+
slug,
|
|
4036
|
+
title,
|
|
4037
|
+
content,
|
|
4038
|
+
format
|
|
4039
|
+
})).dashboard;
|
|
4040
|
+
const url = `${APP_URL$3}/d/${dashboard.org_slug}/${dashboard.slug}`;
|
|
4041
|
+
console.log(pc.green(`Dashboard deployed successfully`));
|
|
4042
|
+
console.log();
|
|
4043
|
+
console.log(` ${pc.bold("Slug")} ${dashboard.slug}`);
|
|
4044
|
+
console.log(` ${pc.bold("Title")} ${dashboard.title}`);
|
|
4045
|
+
console.log(` ${pc.bold("Format")} ${format}`);
|
|
4046
|
+
console.log(` ${pc.bold("Version")} ${dashboard.version}`);
|
|
4047
|
+
console.log(` ${pc.bold("URL")} ${url}`);
|
|
4048
|
+
} catch (err) {
|
|
4049
|
+
console.error(pc.red(`Deploy failed: ${err instanceof Error ? err.message : err}`));
|
|
4050
|
+
process.exit(1);
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
//#endregion
|
|
4055
|
+
//#region src/commands/dashboard/list.ts
|
|
4056
|
+
const APP_URL$2 = process.env.BON_APP_URL || "https://app.bonnard.dev";
|
|
4057
|
+
async function dashboardListCommand() {
|
|
4058
|
+
try {
|
|
4059
|
+
const dashboards = (await get("/api/dashboards")).dashboards;
|
|
4060
|
+
if (dashboards.length === 0) {
|
|
4061
|
+
console.log(pc.dim("No dashboards deployed yet. Run `bon dashboard deploy <file>` to publish."));
|
|
4062
|
+
return;
|
|
4063
|
+
}
|
|
4064
|
+
const slugWidth = Math.max(4, ...dashboards.map((d) => d.slug.length));
|
|
4065
|
+
const titleWidth = Math.max(5, ...dashboards.map((d) => d.title.length));
|
|
4066
|
+
const versionWidth = 7;
|
|
4067
|
+
const dateWidth = 10;
|
|
4068
|
+
const header = [
|
|
4069
|
+
"SLUG".padEnd(slugWidth),
|
|
4070
|
+
"TITLE".padEnd(titleWidth),
|
|
4071
|
+
"VERSION".padEnd(versionWidth),
|
|
4072
|
+
"UPDATED".padEnd(dateWidth),
|
|
4073
|
+
"URL"
|
|
4074
|
+
].join(" ");
|
|
4075
|
+
console.log(pc.dim(header));
|
|
4076
|
+
for (const d of dashboards) {
|
|
4077
|
+
const url = `${APP_URL$2}/d/${d.org_slug}/${d.slug}`;
|
|
4078
|
+
const date = new Date(d.updated_at).toLocaleDateString();
|
|
4079
|
+
const row = [
|
|
4080
|
+
d.slug.padEnd(slugWidth),
|
|
4081
|
+
d.title.padEnd(titleWidth),
|
|
4082
|
+
`v${d.version}`.padEnd(versionWidth),
|
|
4083
|
+
date.padEnd(dateWidth),
|
|
4084
|
+
url
|
|
4085
|
+
].join(" ");
|
|
4086
|
+
console.log(row);
|
|
4087
|
+
}
|
|
4088
|
+
} catch (err) {
|
|
4089
|
+
console.error(pc.red(`Failed to list dashboards: ${err instanceof Error ? err.message : err}`));
|
|
4090
|
+
process.exit(1);
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
//#endregion
|
|
4095
|
+
//#region src/commands/dashboard/remove.ts
|
|
4096
|
+
async function confirm(message) {
|
|
4097
|
+
const rl = createInterface({
|
|
4098
|
+
input: process.stdin,
|
|
4099
|
+
output: process.stdout
|
|
4100
|
+
});
|
|
4101
|
+
return new Promise((resolve) => {
|
|
4102
|
+
rl.question(`${message} (y/N) `, (answer) => {
|
|
4103
|
+
rl.close();
|
|
4104
|
+
resolve(answer.toLowerCase() === "y");
|
|
4105
|
+
});
|
|
4106
|
+
});
|
|
4107
|
+
}
|
|
4108
|
+
async function dashboardRemoveCommand(slug, options) {
|
|
4109
|
+
if (!options.force) {
|
|
4110
|
+
if (!await confirm(`Remove dashboard "${slug}"? This cannot be undone.`)) {
|
|
4111
|
+
console.log(pc.dim("Cancelled."));
|
|
4112
|
+
return;
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
try {
|
|
4116
|
+
await del(`/api/dashboards/${encodeURIComponent(slug)}`);
|
|
4117
|
+
console.log(pc.green(`Dashboard "${slug}" removed.`));
|
|
4118
|
+
} catch (err) {
|
|
4119
|
+
console.error(pc.red(`Failed to remove dashboard: ${err instanceof Error ? err.message : err}`));
|
|
4120
|
+
process.exit(1);
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
|
|
4124
|
+
//#endregion
|
|
4125
|
+
//#region src/commands/dashboard/open.ts
|
|
4126
|
+
const APP_URL$1 = process.env.BON_APP_URL || "https://app.bonnard.dev";
|
|
4127
|
+
async function dashboardOpenCommand(slug) {
|
|
4128
|
+
try {
|
|
4129
|
+
const result = await get(`/api/dashboards/${encodeURIComponent(slug)}`);
|
|
4130
|
+
const url = `${APP_URL$1}/d/${result.dashboard.org_slug}/${result.dashboard.slug}`;
|
|
4131
|
+
console.log(pc.dim(`Opening ${url}`));
|
|
4132
|
+
await open(url);
|
|
4133
|
+
} catch (err) {
|
|
4134
|
+
console.error(pc.red(`Failed to open dashboard: ${err instanceof Error ? err.message : err}`));
|
|
4135
|
+
process.exit(1);
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
//#endregion
|
|
4140
|
+
//#region src/commands/dashboard/dev.ts
|
|
4141
|
+
const APP_URL = process.env.BON_APP_URL || "https://app.bonnard.dev";
|
|
4142
|
+
function loadViewer() {
|
|
4143
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
4144
|
+
const viewerPath = path.resolve(dir, "..", "viewer.html");
|
|
4145
|
+
if (!fs.existsSync(viewerPath)) {
|
|
4146
|
+
console.error(pc.red("Viewer not found. Rebuild the CLI with `pnpm build`."));
|
|
4147
|
+
process.exit(1);
|
|
4148
|
+
}
|
|
4149
|
+
return fs.readFileSync(viewerPath, "utf-8");
|
|
4150
|
+
}
|
|
4151
|
+
async function dashboardDevCommand(file, options) {
|
|
4152
|
+
const filePath = path.resolve(file);
|
|
4153
|
+
if (!fs.existsSync(filePath)) {
|
|
4154
|
+
console.error(pc.red(`File not found: ${file}`));
|
|
4155
|
+
process.exit(1);
|
|
4156
|
+
}
|
|
4157
|
+
const creds = loadCredentials();
|
|
4158
|
+
if (!creds) {
|
|
4159
|
+
console.error(pc.red("Not logged in. Run `bon login` first."));
|
|
4160
|
+
process.exit(1);
|
|
4161
|
+
}
|
|
4162
|
+
const viewerHtml = loadViewer();
|
|
4163
|
+
const sseClients = /* @__PURE__ */ new Set();
|
|
4164
|
+
let debounce = null;
|
|
4165
|
+
fs.watch(filePath, () => {
|
|
4166
|
+
if (debounce) clearTimeout(debounce);
|
|
4167
|
+
debounce = setTimeout(() => {
|
|
4168
|
+
for (const client of sseClients) client.write("data: reload\n\n");
|
|
4169
|
+
}, 50);
|
|
4170
|
+
});
|
|
4171
|
+
const server = http.createServer((req, res) => {
|
|
4172
|
+
const url = req.url || "/";
|
|
4173
|
+
if (url === "/") {
|
|
4174
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
4175
|
+
res.end(viewerHtml);
|
|
4176
|
+
return;
|
|
4177
|
+
}
|
|
4178
|
+
if (url === "/__bon/content") {
|
|
4179
|
+
res.writeHead(200, {
|
|
4180
|
+
"Content-Type": "text/plain",
|
|
4181
|
+
"Cache-Control": "no-cache"
|
|
4182
|
+
});
|
|
4183
|
+
res.end(fs.readFileSync(filePath, "utf-8"));
|
|
4184
|
+
return;
|
|
4185
|
+
}
|
|
4186
|
+
if (url === "/__bon/config") {
|
|
4187
|
+
res.writeHead(200, {
|
|
4188
|
+
"Content-Type": "application/json",
|
|
4189
|
+
"Cache-Control": "no-cache"
|
|
4190
|
+
});
|
|
4191
|
+
res.end(JSON.stringify({
|
|
4192
|
+
token: creds.token,
|
|
4193
|
+
baseUrl: APP_URL
|
|
4194
|
+
}));
|
|
4195
|
+
return;
|
|
4196
|
+
}
|
|
4197
|
+
if (url === "/__bon/events") {
|
|
4198
|
+
res.writeHead(200, {
|
|
4199
|
+
"Content-Type": "text/event-stream",
|
|
4200
|
+
"Cache-Control": "no-cache",
|
|
4201
|
+
Connection: "keep-alive"
|
|
4202
|
+
});
|
|
4203
|
+
sseClients.add(res);
|
|
4204
|
+
req.on("close", () => sseClients.delete(res));
|
|
4205
|
+
return;
|
|
4206
|
+
}
|
|
4207
|
+
res.writeHead(404);
|
|
4208
|
+
res.end();
|
|
4209
|
+
});
|
|
4210
|
+
const port = options.port ? parseInt(options.port, 10) : 0;
|
|
4211
|
+
server.listen(port, () => {
|
|
4212
|
+
const url = `http://localhost:${server.address().port}`;
|
|
4213
|
+
console.log(pc.green(`Dashboard preview running at ${pc.bold(url)}`));
|
|
4214
|
+
console.log(pc.dim(`Watching ${path.basename(filePath)} for changes...\n`));
|
|
4215
|
+
open(url);
|
|
4216
|
+
});
|
|
4217
|
+
}
|
|
4218
|
+
|
|
3873
4219
|
//#endregion
|
|
3874
4220
|
//#region src/bin/bon.ts
|
|
3875
4221
|
const { version } = createRequire(import.meta.url)("../../package.json");
|
|
3876
4222
|
program.name("bon").description("Bonnard semantic layer CLI").version(version);
|
|
3877
|
-
program.command("init").description("Create bon.yaml, bonnard/cubes/, bonnard/views/, .bon/, and agent templates (.claude/, .cursor/)").action(initCommand);
|
|
4223
|
+
program.command("init").description("Create bon.yaml, bonnard/cubes/, bonnard/views/, .bon/, and agent templates (.claude/, .cursor/)").option("--update", "Update agent templates to match installed CLI version").action(initCommand);
|
|
3878
4224
|
program.command("login").description("Authenticate with Bonnard via your browser").action(loginCommand);
|
|
3879
4225
|
program.command("logout").description("Remove stored credentials").action(logoutCommand);
|
|
3880
4226
|
program.command("whoami").description("Show current login status").option("--verify", "Verify session is still valid with the server").action(whoamiCommand);
|
|
@@ -3895,6 +4241,12 @@ const keys = program.command("keys").description("Manage API keys for the Bonnar
|
|
|
3895
4241
|
keys.command("list").description("List all API keys for your organization").action(keysListCommand);
|
|
3896
4242
|
keys.command("create").description("Create a new API key").requiredOption("--name <name>", "Key name (e.g. 'Production SDK')").requiredOption("--type <type>", "Key type: publishable or secret").action(keysCreateCommand);
|
|
3897
4243
|
keys.command("revoke").description("Revoke an API key by name or prefix").argument("<name-or-prefix>", "Key name or key prefix to revoke").action(keysRevokeCommand);
|
|
4244
|
+
const dashboard = program.command("dashboard").description("Manage hosted dashboards");
|
|
4245
|
+
dashboard.command("dev").description("Preview a markdown dashboard locally with live reload").argument("<file>", "Path to dashboard .md file").option("--port <port>", "Server port (default: random available port)").action(dashboardDevCommand);
|
|
4246
|
+
dashboard.command("deploy").description("Deploy an HTML or markdown file as a hosted dashboard").argument("<file>", "Path to HTML or markdown file").option("--slug <slug>", "Dashboard slug (defaults to filename)").option("--title <title>", "Dashboard title (defaults to <title> tag or slug)").action(dashboardDeployCommand);
|
|
4247
|
+
dashboard.command("list").description("List deployed dashboards").action(dashboardListCommand);
|
|
4248
|
+
dashboard.command("remove").description("Remove a deployed dashboard").argument("<slug>", "Dashboard slug to remove").option("--force", "Skip confirmation prompt").action(dashboardRemoveCommand);
|
|
4249
|
+
dashboard.command("open").description("Open a deployed dashboard in the browser").argument("<slug>", "Dashboard slug to open").action(dashboardOpenCommand);
|
|
3898
4250
|
const metabase = program.command("metabase").description("Connect to and explore Metabase content");
|
|
3899
4251
|
metabase.command("connect").description("Configure Metabase API connection").option("--url <url>", "Metabase instance URL").option("--api-key <key>", "Metabase API key").option("--force", "Overwrite existing configuration").action(metabaseConnectCommand);
|
|
3900
4252
|
metabase.command("explore").description("Browse Metabase databases, collections, cards, and dashboards").argument("[resource]", "databases, collections, cards, dashboards, card, dashboard, database, table, collection").argument("[id]", "Resource ID (e.g. card <id>, dashboard <id>, database <id>, table <id>, collection <id>)").action(metabaseExploreCommand);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Dashboards let your team track key metrics without leaving the Bonnard app. Define queries once in markdown, deploy them, and every viewer gets live, governed data — no separate BI tool needed. Filters, formatting, and layout are all declared in the same file.
|
|
8
8
|
|
|
9
|
-
A dashboard is a markdown file with YAML frontmatter, query blocks, and chart components. Write it as a `.md` file,
|
|
9
|
+
A dashboard is a markdown file with YAML frontmatter, query blocks, and chart components. Write it as a `.md` file, preview locally with `bon dashboard dev`, and deploy with `bon dashboard deploy`.
|
|
10
10
|
|
|
11
11
|
## Format
|
|
12
12
|
|
|
@@ -48,7 +48,6 @@ The YAML frontmatter is required and must include `title`:
|
|
|
48
48
|
---
|
|
49
49
|
title: Revenue Dashboard # Required
|
|
50
50
|
description: Monthly trends # Optional
|
|
51
|
-
slug: revenue-dashboard # Optional (derived from title if omitted)
|
|
52
51
|
---
|
|
53
52
|
```
|
|
54
53
|
|
|
@@ -56,49 +55,47 @@ slug: revenue-dashboard # Optional (derived from title if omitted)
|
|
|
56
55
|
|-------|----------|-------------|
|
|
57
56
|
| `title` | Yes | Dashboard title displayed in the viewer and listings |
|
|
58
57
|
| `description` | No | Short description shown in dashboard listings |
|
|
59
|
-
|
|
58
|
+
|
|
59
|
+
## Local Preview
|
|
60
|
+
|
|
61
|
+
Preview a dashboard locally with live reload before deploying. Requires `bon login`.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bon dashboard dev revenue.md
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This starts a local server, opens your browser, and auto-reloads when you save changes. Queries run against your deployed semantic layer using your own credentials — governance policies apply.
|
|
60
68
|
|
|
61
69
|
## Deployment
|
|
62
70
|
|
|
63
71
|
Deploy from the command line or via MCP tools. Each deploy auto-versions the dashboard so you can roll back if needed.
|
|
64
72
|
|
|
65
73
|
```bash
|
|
66
|
-
# Deploy a
|
|
74
|
+
# Deploy a markdown dashboard
|
|
67
75
|
bon dashboard deploy revenue.md
|
|
68
76
|
|
|
69
|
-
# Deploy all dashboards in a directory
|
|
70
|
-
bon dashboard deploy dashboards/
|
|
71
|
-
|
|
72
77
|
# List deployed dashboards
|
|
73
78
|
bon dashboard list
|
|
74
79
|
|
|
80
|
+
# Open in browser
|
|
81
|
+
bon dashboard open revenue
|
|
82
|
+
|
|
75
83
|
# Remove a dashboard
|
|
76
|
-
bon dashboard remove revenue
|
|
84
|
+
bon dashboard remove revenue
|
|
77
85
|
```
|
|
78
86
|
|
|
87
|
+
Options:
|
|
88
|
+
- `--slug <slug>` — custom URL slug (default: derived from filename)
|
|
89
|
+
- `--title <title>` — dashboard title (default: from frontmatter)
|
|
90
|
+
|
|
79
91
|
Via MCP tools, agents can use `deploy_dashboard` with the markdown content as a string.
|
|
80
92
|
|
|
81
93
|
## Versioning
|
|
82
94
|
|
|
83
|
-
Every deployment auto-increments the version number and saves a snapshot. You can view version history and restore previous versions
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
# Via MCP tools:
|
|
87
|
-
# get_dashboard with version parameter to fetch a specific version
|
|
88
|
-
# deploy_dashboard with slug + restore_version to roll back
|
|
89
|
-
```
|
|
95
|
+
Every deployment auto-increments the version number and saves a snapshot. You can view version history and restore previous versions via MCP tools (`get_dashboard` with version parameter, `deploy_dashboard` with `restore_version`).
|
|
90
96
|
|
|
91
97
|
Restoring a version creates a new version (e.g. restoring v2 from v5 creates v6 with v2's content). Version history is never deleted — only `remove_dashboard` deletes all history.
|
|
92
98
|
|
|
93
|
-
## Sharing
|
|
94
|
-
|
|
95
|
-
Dashboard viewers include a **Share** menu in the header with:
|
|
96
|
-
|
|
97
|
-
- **Copy link** — copies the current URL including any active filter state
|
|
98
|
-
- **Print to PDF** — opens the browser print dialog for PDF export
|
|
99
|
-
|
|
100
|
-
Filter state (DateRange presets, Dropdown selections) is encoded in URL query params, so shared links preserve the exact filtered view the sender was looking at.
|
|
101
|
-
|
|
102
99
|
## Governance
|
|
103
100
|
|
|
104
101
|
Dashboard queries respect the same governance policies as all other queries. When a user views a dashboard:
|