@bonnard/cli 0.2.16 → 0.3.1

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
@@ -174,6 +174,13 @@ Handles automatic datasource synchronisation and skips interactive prompts. Fits
174
174
  | `bon query` | Run queries from the terminal (JSON or SQL) |
175
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
@@ -557,36 +557,120 @@ function loadJsonTemplate(relativePath) {
557
557
  return JSON.parse(content);
558
558
  }
559
559
  /**
560
- * 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
561
563
  */
562
- function writeTemplateFile(content, targetPath, createdFiles) {
564
+ function writeTemplateFile(content, targetPath, results, mode = "init") {
565
+ const relativePath = path.relative(process.cwd(), targetPath);
563
566
  if (fs.existsSync(targetPath)) {
564
- 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")) {
565
580
  fs.appendFileSync(targetPath, `\n\n${content}`);
566
- createdFiles.push(`${path.relative(process.cwd(), targetPath)} (appended)`);
581
+ results.push({
582
+ action: "appended",
583
+ path: relativePath
584
+ });
567
585
  }
568
586
  } else {
569
587
  fs.writeFileSync(targetPath, content);
570
- createdFiles.push(path.relative(process.cwd(), targetPath));
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")) {
621
+ fs.appendFileSync(targetPath, `\n\n${content}`);
622
+ results.push({
623
+ action: "appended",
624
+ path: relativePath
625
+ });
626
+ }
627
+ } else {
628
+ fs.writeFileSync(targetPath, content);
629
+ results.push({
630
+ action: mode === "update" ? "added" : "created",
631
+ path: relativePath
632
+ });
571
633
  }
572
634
  }
573
635
  /**
574
636
  * Merge settings.json, preserving existing settings
575
637
  */
576
- function mergeSettingsJson(templateSettings, targetPath, createdFiles) {
638
+ function mergeSettingsJson(templateSettings, targetPath, results, mode = "init") {
639
+ const relativePath = path.relative(process.cwd(), targetPath);
577
640
  if (fs.existsSync(targetPath)) {
578
- const existingContent = JSON.parse(fs.readFileSync(targetPath, "utf-8"));
641
+ const existingRaw = fs.readFileSync(targetPath, "utf-8");
642
+ const existingContent = JSON.parse(existingRaw);
579
643
  const templatePerms = templateSettings.permissions;
580
644
  if (templatePerms?.allow) {
581
645
  existingContent.permissions = existingContent.permissions || {};
582
646
  existingContent.permissions.allow = existingContent.permissions.allow || [];
583
647
  for (const permission of templatePerms.allow) if (!existingContent.permissions.allow.includes(permission)) existingContent.permissions.allow.push(permission);
584
648
  }
585
- fs.writeFileSync(targetPath, JSON.stringify(existingContent, null, 2) + "\n");
586
- 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
+ }
587
668
  } else {
588
669
  fs.writeFileSync(targetPath, JSON.stringify(templateSettings, null, 2) + "\n");
589
- createdFiles.push(path.relative(process.cwd(), targetPath));
670
+ results.push({
671
+ action: mode === "update" ? "added" : "created",
672
+ path: relativePath
673
+ });
590
674
  }
591
675
  }
592
676
  /**
@@ -603,8 +687,8 @@ alwaysApply: ${alwaysApply}
603
687
  /**
604
688
  * Create agent templates (Claude Code, Cursor, and Codex)
605
689
  */
606
- function createAgentTemplates(cwd, env) {
607
- const createdFiles = [];
690
+ function createAgentTemplates(cwd, env, mode = "init") {
691
+ const results = [];
608
692
  let sharedBonnard = loadTemplate("shared/bonnard.md");
609
693
  if (env) sharedBonnard += "\n\n" + generateProjectContext(env);
610
694
  const claudeRulesDir = path.join(cwd, ".claude", "rules");
@@ -614,35 +698,64 @@ function createAgentTemplates(cwd, env) {
614
698
  fs.mkdirSync(path.join(claudeSkillsDir, "bonnard-metabase-migrate"), { recursive: true });
615
699
  fs.mkdirSync(path.join(claudeSkillsDir, "bonnard-design-guide"), { recursive: true });
616
700
  fs.mkdirSync(path.join(claudeSkillsDir, "bonnard-build-dashboard"), { recursive: true });
617
- writeTemplateFile(sharedBonnard, path.join(claudeRulesDir, "bonnard.md"), createdFiles);
618
- writeTemplateFile(loadTemplate("claude/skills/bonnard-get-started/SKILL.md"), path.join(claudeSkillsDir, "bonnard-get-started", "SKILL.md"), createdFiles);
619
- writeTemplateFile(loadTemplate("claude/skills/bonnard-metabase-migrate/SKILL.md"), path.join(claudeSkillsDir, "bonnard-metabase-migrate", "SKILL.md"), createdFiles);
620
- writeTemplateFile(loadTemplate("claude/skills/bonnard-design-guide/SKILL.md"), path.join(claudeSkillsDir, "bonnard-design-guide", "SKILL.md"), createdFiles);
621
- writeTemplateFile(loadTemplate("claude/skills/bonnard-build-dashboard/SKILL.md"), path.join(claudeSkillsDir, "bonnard-build-dashboard", "SKILL.md"), createdFiles);
622
- mergeSettingsJson(loadJsonTemplate("claude/settings.json"), path.join(cwd, ".claude", "settings.json"), createdFiles);
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);
623
707
  const cursorRulesDir = path.join(cwd, ".cursor", "rules");
624
708
  fs.mkdirSync(cursorRulesDir, { recursive: true });
625
- writeTemplateFile(withCursorFrontmatter(sharedBonnard, "Bonnard semantic layer project context", true), path.join(cursorRulesDir, "bonnard.mdc"), createdFiles);
626
- writeTemplateFile(loadTemplate("cursor/rules/bonnard-get-started.mdc"), path.join(cursorRulesDir, "bonnard-get-started.mdc"), createdFiles);
627
- writeTemplateFile(loadTemplate("cursor/rules/bonnard-metabase-migrate.mdc"), path.join(cursorRulesDir, "bonnard-metabase-migrate.mdc"), createdFiles);
628
- writeTemplateFile(loadTemplate("cursor/rules/bonnard-design-guide.mdc"), path.join(cursorRulesDir, "bonnard-design-guide.mdc"), createdFiles);
629
- writeTemplateFile(loadTemplate("cursor/rules/bonnard-build-dashboard.mdc"), path.join(cursorRulesDir, "bonnard-build-dashboard.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);
630
714
  const codexSkillsDir = path.join(cwd, ".agents", "skills");
631
715
  fs.mkdirSync(path.join(codexSkillsDir, "bonnard-get-started"), { recursive: true });
632
716
  fs.mkdirSync(path.join(codexSkillsDir, "bonnard-metabase-migrate"), { recursive: true });
633
717
  fs.mkdirSync(path.join(codexSkillsDir, "bonnard-design-guide"), { recursive: true });
634
718
  fs.mkdirSync(path.join(codexSkillsDir, "bonnard-build-dashboard"), { recursive: true });
635
- writeTemplateFile(sharedBonnard, path.join(cwd, "AGENTS.md"), createdFiles);
636
- writeTemplateFile(loadTemplate("claude/skills/bonnard-get-started/SKILL.md"), path.join(codexSkillsDir, "bonnard-get-started", "SKILL.md"), createdFiles);
637
- writeTemplateFile(loadTemplate("claude/skills/bonnard-metabase-migrate/SKILL.md"), path.join(codexSkillsDir, "bonnard-metabase-migrate", "SKILL.md"), createdFiles);
638
- writeTemplateFile(loadTemplate("claude/skills/bonnard-design-guide/SKILL.md"), path.join(codexSkillsDir, "bonnard-design-guide", "SKILL.md"), createdFiles);
639
- writeTemplateFile(loadTemplate("claude/skills/bonnard-build-dashboard/SKILL.md"), path.join(codexSkillsDir, "bonnard-build-dashboard", "SKILL.md"), createdFiles);
640
- return createdFiles;
641
- }
642
- async function initCommand() {
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
+ }
732
+ }
733
+ async function initCommand(options = {}) {
643
734
  const cwd = process.cwd();
644
- const projectName = path.basename(cwd);
645
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);
646
759
  if (fs.existsSync(paths.config)) {
647
760
  console.log(pc.red("A bon.yaml already exists in this directory."));
648
761
  process.exit(1);
@@ -665,7 +778,7 @@ async function initCommand() {
665
778
  if (agentFiles.length > 0) {
666
779
  console.log();
667
780
  console.log(pc.bold("Agent support:"));
668
- for (const file of agentFiles) console.log(` ${pc.dim(file)}`);
781
+ for (const file of agentFiles) console.log(` ${pc.dim(formatFileResult(file))}`);
669
782
  }
670
783
  if (env.tools.length > 0 || env.warehouse) {
671
784
  console.log();
@@ -677,12 +790,12 @@ async function initCommand() {
677
790
 
678
791
  //#endregion
679
792
  //#region src/commands/login.ts
680
- const APP_URL$3 = process.env.BON_APP_URL || "https://app.bonnard.dev";
793
+ const APP_URL$4 = process.env.BON_APP_URL || "https://app.bonnard.dev";
681
794
  const TIMEOUT_MS = 120 * 1e3;
682
795
  async function loginCommand() {
683
796
  const state = crypto.randomUUID();
684
797
  const { port, close } = await startCallbackServer(state);
685
- const url = `${APP_URL$3}/auth/device?state=${state}&port=${port}`;
798
+ const url = `${APP_URL$4}/auth/device?state=${state}&port=${port}`;
686
799
  console.log(pc.dim(`Opening browser to ${url}`));
687
800
  const open = (await import("open")).default;
688
801
  await open(url);
@@ -3879,15 +3992,24 @@ async function keysRevokeCommand(nameOrPrefix) {
3879
3992
 
3880
3993
  //#endregion
3881
3994
  //#region src/commands/dashboard/deploy.ts
3882
- const APP_URL$2 = process.env.BON_APP_URL || "https://app.bonnard.dev";
3995
+ const APP_URL$3 = process.env.BON_APP_URL || "https://app.bonnard.dev";
3883
3996
  /**
3884
3997
  * Extract <title> content from HTML string
3885
3998
  */
3886
- function extractTitle(html) {
3999
+ function extractHtmlTitle(html) {
3887
4000
  const match = html.match(/<title[^>]*>(.*?)<\/title>/is);
3888
4001
  return match ? match[1].trim() : null;
3889
4002
  }
3890
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
+ /**
3891
4013
  * Derive slug from filename: strip extension, lowercase, replace non-alphanumeric with hyphens
3892
4014
  */
3893
4015
  function slugFromFilename(filename) {
@@ -3904,20 +4026,23 @@ async function dashboardDeployCommand(file, options) {
3904
4026
  console.error(pc.red("File exceeds 2MB limit."));
3905
4027
  process.exit(1);
3906
4028
  }
4029
+ const format = path.extname(filePath).toLowerCase() === ".md" ? "markdown" : "html";
3907
4030
  const slug = options.slug || slugFromFilename(file);
3908
- const title = options.title || extractTitle(content) || slug;
3909
- console.log(pc.dim(`Deploying dashboard "${slug}"...`));
4031
+ const title = options.title || (format === "markdown" ? extractFrontmatterTitle(content) || slug : extractHtmlTitle(content) || slug);
4032
+ console.log(pc.dim(`Deploying ${format} dashboard "${slug}"...`));
3910
4033
  try {
3911
4034
  const dashboard = (await post("/api/dashboards", {
3912
4035
  slug,
3913
4036
  title,
3914
- content
4037
+ content,
4038
+ format
3915
4039
  })).dashboard;
3916
- const url = `${APP_URL$2}/d/${dashboard.org_slug}/${dashboard.slug}`;
4040
+ const url = `${APP_URL$3}/d/${dashboard.org_slug}/${dashboard.slug}`;
3917
4041
  console.log(pc.green(`Dashboard deployed successfully`));
3918
4042
  console.log();
3919
4043
  console.log(` ${pc.bold("Slug")} ${dashboard.slug}`);
3920
4044
  console.log(` ${pc.bold("Title")} ${dashboard.title}`);
4045
+ console.log(` ${pc.bold("Format")} ${format}`);
3921
4046
  console.log(` ${pc.bold("Version")} ${dashboard.version}`);
3922
4047
  console.log(` ${pc.bold("URL")} ${url}`);
3923
4048
  } catch (err) {
@@ -3928,7 +4053,7 @@ async function dashboardDeployCommand(file, options) {
3928
4053
 
3929
4054
  //#endregion
3930
4055
  //#region src/commands/dashboard/list.ts
3931
- const APP_URL$1 = process.env.BON_APP_URL || "https://app.bonnard.dev";
4056
+ const APP_URL$2 = process.env.BON_APP_URL || "https://app.bonnard.dev";
3932
4057
  async function dashboardListCommand() {
3933
4058
  try {
3934
4059
  const dashboards = (await get("/api/dashboards")).dashboards;
@@ -3949,7 +4074,7 @@ async function dashboardListCommand() {
3949
4074
  ].join(" ");
3950
4075
  console.log(pc.dim(header));
3951
4076
  for (const d of dashboards) {
3952
- const url = `${APP_URL$1}/d/${d.org_slug}/${d.slug}`;
4077
+ const url = `${APP_URL$2}/d/${d.org_slug}/${d.slug}`;
3953
4078
  const date = new Date(d.updated_at).toLocaleDateString();
3954
4079
  const row = [
3955
4080
  d.slug.padEnd(slugWidth),
@@ -3998,11 +4123,11 @@ async function dashboardRemoveCommand(slug, options) {
3998
4123
 
3999
4124
  //#endregion
4000
4125
  //#region src/commands/dashboard/open.ts
4001
- const APP_URL = process.env.BON_APP_URL || "https://app.bonnard.dev";
4126
+ const APP_URL$1 = process.env.BON_APP_URL || "https://app.bonnard.dev";
4002
4127
  async function dashboardOpenCommand(slug) {
4003
4128
  try {
4004
4129
  const result = await get(`/api/dashboards/${encodeURIComponent(slug)}`);
4005
- const url = `${APP_URL}/d/${result.dashboard.org_slug}/${result.dashboard.slug}`;
4130
+ const url = `${APP_URL$1}/d/${result.dashboard.org_slug}/${result.dashboard.slug}`;
4006
4131
  console.log(pc.dim(`Opening ${url}`));
4007
4132
  await open(url);
4008
4133
  } catch (err) {
@@ -4011,11 +4136,91 @@ async function dashboardOpenCommand(slug) {
4011
4136
  }
4012
4137
  }
4013
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
+
4014
4219
  //#endregion
4015
4220
  //#region src/bin/bon.ts
4016
4221
  const { version } = createRequire(import.meta.url)("../../package.json");
4017
4222
  program.name("bon").description("Bonnard semantic layer CLI").version(version);
4018
- 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);
4019
4224
  program.command("login").description("Authenticate with Bonnard via your browser").action(loginCommand);
4020
4225
  program.command("logout").description("Remove stored credentials").action(logoutCommand);
4021
4226
  program.command("whoami").description("Show current login status").option("--verify", "Verify session is still valid with the server").action(whoamiCommand);
@@ -4036,8 +4241,9 @@ const keys = program.command("keys").description("Manage API keys for the Bonnar
4036
4241
  keys.command("list").description("List all API keys for your organization").action(keysListCommand);
4037
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);
4038
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);
4039
- const dashboard = program.command("dashboard").description("Manage hosted HTML dashboards");
4040
- dashboard.command("deploy").description("Deploy an HTML file as a hosted dashboard").argument("<file>", "Path to HTML file").option("--slug <slug>", "Dashboard slug (defaults to filename)").option("--title <title>", "Dashboard title (defaults to <title> tag or slug)").action(dashboardDeployCommand);
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);
4041
4247
  dashboard.command("list").description("List deployed dashboards").action(dashboardListCommand);
4042
4248
  dashboard.command("remove").description("Remove a deployed dashboard").argument("<slug>", "Dashboard slug to remove").option("--force", "Skip confirmation prompt").action(dashboardRemoveCommand);
4043
4249
  dashboard.command("open").description("Open a deployed dashboard in the browser").argument("<slug>", "Dashboard slug to open").action(dashboardOpenCommand);