@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 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 setup # Configure MCP server
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 setup` | Configure MCP server for agent access |
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, appending if target exists and doesn't already have Bonnard content
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, createdFiles) {
564
+ function writeTemplateFile(content, targetPath, results, mode = "init") {
565
+ const relativePath = path.relative(process.cwd(), targetPath);
561
566
  if (fs.existsSync(targetPath)) {
562
- if (!fs.readFileSync(targetPath, "utf-8").includes("# Bonnard")) {
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
- createdFiles.push(`${path.relative(process.cwd(), targetPath)} (appended)`);
622
+ results.push({
623
+ action: "appended",
624
+ path: relativePath
625
+ });
565
626
  }
566
627
  } else {
567
628
  fs.writeFileSync(targetPath, content);
568
- createdFiles.push(path.relative(process.cwd(), targetPath));
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, createdFiles) {
638
+ function mergeSettingsJson(templateSettings, targetPath, results, mode = "init") {
639
+ const relativePath = path.relative(process.cwd(), targetPath);
575
640
  if (fs.existsSync(targetPath)) {
576
- const existingContent = JSON.parse(fs.readFileSync(targetPath, "utf-8"));
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
- fs.writeFileSync(targetPath, JSON.stringify(existingContent, null, 2) + "\n");
584
- createdFiles.push(`${path.relative(process.cwd(), targetPath)} (merged)`);
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
- createdFiles.push(path.relative(process.cwd(), targetPath));
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 createdFiles = [];
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
- writeTemplateFile(sharedBonnard, path.join(claudeRulesDir, "bonnard.md"), createdFiles);
615
- writeTemplateFile(loadTemplate("claude/skills/bonnard-get-started/SKILL.md"), path.join(claudeSkillsDir, "bonnard-get-started", "SKILL.md"), createdFiles);
616
- writeTemplateFile(loadTemplate("claude/skills/bonnard-metabase-migrate/SKILL.md"), path.join(claudeSkillsDir, "bonnard-metabase-migrate", "SKILL.md"), createdFiles);
617
- writeTemplateFile(loadTemplate("claude/skills/bonnard-design-guide/SKILL.md"), path.join(claudeSkillsDir, "bonnard-design-guide", "SKILL.md"), createdFiles);
618
- mergeSettingsJson(loadJsonTemplate("claude/settings.json"), path.join(cwd, ".claude", "settings.json"), createdFiles);
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"), createdFiles);
622
- writeTemplateFile(loadTemplate("cursor/rules/bonnard-get-started.mdc"), path.join(cursorRulesDir, "bonnard-get-started.mdc"), createdFiles);
623
- writeTemplateFile(loadTemplate("cursor/rules/bonnard-metabase-migrate.mdc"), path.join(cursorRulesDir, "bonnard-metabase-migrate.mdc"), createdFiles);
624
- writeTemplateFile(loadTemplate("cursor/rules/bonnard-design-guide.mdc"), path.join(cursorRulesDir, "bonnard-design-guide.mdc"), createdFiles);
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
- writeTemplateFile(sharedBonnard, path.join(cwd, "AGENTS.md"), createdFiles);
630
- writeTemplateFile(loadTemplate("claude/skills/bonnard-get-started/SKILL.md"), path.join(codexSkillsDir, "bonnard-get-started", "SKILL.md"), createdFiles);
631
- writeTemplateFile(loadTemplate("claude/skills/bonnard-metabase-migrate/SKILL.md"), path.join(codexSkillsDir, "bonnard-metabase-migrate", "SKILL.md"), createdFiles);
632
- writeTemplateFile(loadTemplate("claude/skills/bonnard-design-guide/SKILL.md"), path.join(codexSkillsDir, "bonnard-design-guide", "SKILL.md"), createdFiles);
633
- return createdFiles;
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, deploy with `bon dashboard deploy`, and view it in the Bonnard web app.
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
- | `slug` | No | URL-safe identifier. Auto-derived from title if omitted |
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 single dashboard
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-dashboard
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: