@bonnard/cli 0.2.6 → 0.2.7

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.
Files changed (2) hide show
  1. package/dist/bin/bon.mjs +142 -65
  2. package/package.json +1 -1
package/dist/bin/bon.mjs CHANGED
@@ -2698,11 +2698,11 @@ async function showOverview(client) {
2698
2698
  console.log(pc.dim(" bon metabase explore collections"));
2699
2699
  console.log(pc.dim(" bon metabase explore cards"));
2700
2700
  console.log(pc.dim(" bon metabase explore dashboards"));
2701
- console.log(pc.dim(" bon metabase explore card <id>"));
2702
- console.log(pc.dim(" bon metabase explore dashboard <id>"));
2703
- console.log(pc.dim(" bon metabase explore database <id>"));
2704
- console.log(pc.dim(" bon metabase explore table <id>"));
2705
- console.log(pc.dim(" bon metabase explore collection <id>"));
2701
+ console.log(pc.dim(" bon metabase explore card <id-or-name>"));
2702
+ console.log(pc.dim(" bon metabase explore dashboard <id-or-name>"));
2703
+ console.log(pc.dim(" bon metabase explore database <id-or-name>"));
2704
+ console.log(pc.dim(" bon metabase explore table <id-or-name>"));
2705
+ console.log(pc.dim(" bon metabase explore collection <id-or-name>"));
2706
2706
  }
2707
2707
  async function showDatabases(client) {
2708
2708
  const databases = await client.getDatabases();
@@ -2778,7 +2778,7 @@ async function showCards(client) {
2778
2778
  console.log();
2779
2779
  }
2780
2780
  if (models.length === 0 && metrics.length === 0 && questions.length === 0) console.log(pc.dim(" No cards found."));
2781
- console.log(pc.dim("View details: bon metabase explore card <id>"));
2781
+ console.log(pc.dim("View details: bon metabase explore card <id-or-name>"));
2782
2782
  }
2783
2783
  async function showCardDetail(client, id) {
2784
2784
  const card = await client.getCard(id);
@@ -2835,7 +2835,7 @@ async function showDashboards(client) {
2835
2835
  console.log(` ${pc.dim(padColumn("ID", 6))}${pc.dim("NAME")}`);
2836
2836
  for (const d of active) console.log(` ${padColumn(String(d.id), 6)}${d.name}`);
2837
2837
  console.log();
2838
- console.log(pc.dim("View details: bon metabase explore dashboard <id>"));
2838
+ console.log(pc.dim("View details: bon metabase explore dashboard <id-or-name>"));
2839
2839
  }
2840
2840
  async function showDashboardDetail(client, id) {
2841
2841
  const [dashboard, allCards] = await Promise.all([client.getDashboard(id), client.getCards()]);
@@ -2895,7 +2895,7 @@ async function showDatabaseDetail(client, id) {
2895
2895
  }
2896
2896
  console.log();
2897
2897
  }
2898
- console.log(pc.dim("View table fields: bon metabase explore table <id>"));
2898
+ console.log(pc.dim("View table fields: bon metabase explore table <id-or-name>"));
2899
2899
  }
2900
2900
  function classifyFieldType(field) {
2901
2901
  const bt = field.base_type || "";
@@ -3008,8 +3008,105 @@ async function showCollectionDetail(client, id) {
3008
3008
  console.log();
3009
3009
  }
3010
3010
  if (cardItems.length === 0 && dashboardItems.length === 0) console.log(pc.dim(" No items in this collection."));
3011
- console.log(pc.dim("View card SQL: bon metabase explore card <id>"));
3012
- console.log(pc.dim("View dashboard: bon metabase explore dashboard <id>"));
3011
+ console.log(pc.dim("View card SQL: bon metabase explore card <id-or-name>"));
3012
+ console.log(pc.dim("View dashboard: bon metabase explore dashboard <id-or-name>"));
3013
+ }
3014
+ function isNumericId(value) {
3015
+ return /^\d+$/.test(value);
3016
+ }
3017
+ function showDisambiguation(resource, matches) {
3018
+ console.error(pc.yellow(`Multiple ${resource}s match that name:\n`));
3019
+ for (const m of matches) console.log(` ${padColumn(String(m.id), 8)}${m.label}`);
3020
+ console.log();
3021
+ console.log(pc.dim(`Use the numeric ID to be specific: bon metabase explore ${resource} <id>`));
3022
+ process.exit(1);
3023
+ }
3024
+ async function resolveCardId(client, input) {
3025
+ if (isNumericId(input)) return parseInt(input, 10);
3026
+ const cards = await client.getCards();
3027
+ const needle = input.toLowerCase();
3028
+ const matches = cards.filter((c) => c.name?.toLowerCase().includes(needle));
3029
+ if (matches.length === 0) {
3030
+ console.error(pc.red(`No card found matching "${input}"`));
3031
+ process.exit(1);
3032
+ }
3033
+ if (matches.length === 1) return matches[0].id;
3034
+ showDisambiguation("card", matches.map((c) => ({
3035
+ id: c.id,
3036
+ label: c.name
3037
+ })));
3038
+ }
3039
+ async function resolveDashboardId(client, input) {
3040
+ if (isNumericId(input)) return parseInt(input, 10);
3041
+ const dashboards = await client.getDashboards();
3042
+ const needle = input.toLowerCase();
3043
+ const matches = dashboards.filter((d) => d.name?.toLowerCase().includes(needle));
3044
+ if (matches.length === 0) {
3045
+ console.error(pc.red(`No dashboard found matching "${input}"`));
3046
+ process.exit(1);
3047
+ }
3048
+ if (matches.length === 1) return matches[0].id;
3049
+ showDisambiguation("dashboard", matches.map((d) => ({
3050
+ id: d.id,
3051
+ label: d.name
3052
+ })));
3053
+ }
3054
+ async function resolveDatabaseId(client, input) {
3055
+ if (isNumericId(input)) return parseInt(input, 10);
3056
+ const databases = await client.getDatabases();
3057
+ const needle = input.toLowerCase();
3058
+ const matches = databases.filter((d) => d.name?.toLowerCase().includes(needle));
3059
+ if (matches.length === 0) {
3060
+ console.error(pc.red(`No database found matching "${input}"`));
3061
+ process.exit(1);
3062
+ }
3063
+ if (matches.length === 1) return matches[0].id;
3064
+ showDisambiguation("database", matches.map((d) => ({
3065
+ id: d.id,
3066
+ label: `${d.name} (${d.engine})`
3067
+ })));
3068
+ }
3069
+ async function resolveTableId(client, input) {
3070
+ if (isNumericId(input)) return parseInt(input, 10);
3071
+ const databases = await client.getDatabases();
3072
+ const needle = input.toLowerCase();
3073
+ const matches = [];
3074
+ for (const db of databases) {
3075
+ const meta = await client.getDatabaseMetadata(db.id);
3076
+ for (const t of meta.tables) {
3077
+ if (t.visibility_type === "hidden" || t.visibility_type === "retired") continue;
3078
+ if (t.name.toLowerCase().includes(needle)) matches.push({
3079
+ id: t.id,
3080
+ name: t.name,
3081
+ schema: t.schema,
3082
+ dbName: db.name
3083
+ });
3084
+ }
3085
+ }
3086
+ if (matches.length === 0) {
3087
+ console.error(pc.red(`No table found matching "${input}"`));
3088
+ process.exit(1);
3089
+ }
3090
+ if (matches.length === 1) return matches[0].id;
3091
+ showDisambiguation("table", matches.map((m) => ({
3092
+ id: m.id,
3093
+ label: `${m.dbName} / ${m.schema}.${m.name}`
3094
+ })));
3095
+ }
3096
+ async function resolveCollectionId(client, input) {
3097
+ if (isNumericId(input)) return parseInt(input, 10);
3098
+ const collections = await client.getCollections();
3099
+ const needle = input.toLowerCase();
3100
+ const matches = collections.filter((c) => typeof c.id === "number" && c.name?.toLowerCase().includes(needle));
3101
+ if (matches.length === 0) {
3102
+ console.error(pc.red(`No collection found matching "${input}"`));
3103
+ process.exit(1);
3104
+ }
3105
+ if (matches.length === 1) return matches[0].id;
3106
+ showDisambiguation("collection", matches.map((c) => ({
3107
+ id: c.id,
3108
+ label: c.name
3109
+ })));
3013
3110
  }
3014
3111
  const RESOURCES = [
3015
3112
  "databases",
@@ -3047,71 +3144,41 @@ async function metabaseExploreCommand(resource, id) {
3047
3144
  case "dashboards":
3048
3145
  await showDashboards(client);
3049
3146
  break;
3050
- case "card": {
3147
+ case "card":
3051
3148
  if (!id) {
3052
- console.error(pc.red("Card ID required: bon metabase explore card <id>"));
3149
+ console.error(pc.red("Usage: bon metabase explore card <id-or-name>"));
3053
3150
  process.exit(1);
3054
3151
  }
3055
- const cardId = parseInt(id, 10);
3056
- if (isNaN(cardId)) {
3057
- console.error(pc.red(`Invalid card ID: ${id}`));
3058
- process.exit(1);
3059
- }
3060
- await showCardDetail(client, cardId);
3152
+ await showCardDetail(client, await resolveCardId(client, id));
3061
3153
  break;
3062
- }
3063
- case "dashboard": {
3154
+ case "dashboard":
3064
3155
  if (!id) {
3065
- console.error(pc.red("Dashboard ID required: bon metabase explore dashboard <id>"));
3156
+ console.error(pc.red("Usage: bon metabase explore dashboard <id-or-name>"));
3066
3157
  process.exit(1);
3067
3158
  }
3068
- const dashId = parseInt(id, 10);
3069
- if (isNaN(dashId)) {
3070
- console.error(pc.red(`Invalid dashboard ID: ${id}`));
3071
- process.exit(1);
3072
- }
3073
- await showDashboardDetail(client, dashId);
3159
+ await showDashboardDetail(client, await resolveDashboardId(client, id));
3074
3160
  break;
3075
- }
3076
- case "database": {
3161
+ case "database":
3077
3162
  if (!id) {
3078
- console.error(pc.red("Database ID required: bon metabase explore database <id>"));
3079
- process.exit(1);
3080
- }
3081
- const dbId = parseInt(id, 10);
3082
- if (isNaN(dbId)) {
3083
- console.error(pc.red(`Invalid database ID: ${id}`));
3163
+ console.error(pc.red("Usage: bon metabase explore database <id-or-name>"));
3084
3164
  process.exit(1);
3085
3165
  }
3086
- await showDatabaseDetail(client, dbId);
3166
+ await showDatabaseDetail(client, await resolveDatabaseId(client, id));
3087
3167
  break;
3088
- }
3089
- case "table": {
3168
+ case "table":
3090
3169
  if (!id) {
3091
- console.error(pc.red("Table ID required: bon metabase explore table <id>"));
3170
+ console.error(pc.red("Usage: bon metabase explore table <id-or-name>"));
3092
3171
  process.exit(1);
3093
3172
  }
3094
- const tableId = parseInt(id, 10);
3095
- if (isNaN(tableId)) {
3096
- console.error(pc.red(`Invalid table ID: ${id}`));
3097
- process.exit(1);
3098
- }
3099
- await showTableDetail(client, tableId);
3173
+ await showTableDetail(client, await resolveTableId(client, id));
3100
3174
  break;
3101
- }
3102
- case "collection": {
3175
+ case "collection":
3103
3176
  if (!id) {
3104
- console.error(pc.red("Collection ID required: bon metabase explore collection <id>"));
3177
+ console.error(pc.red("Usage: bon metabase explore collection <id-or-name>"));
3105
3178
  process.exit(1);
3106
3179
  }
3107
- const colId = parseInt(id, 10);
3108
- if (isNaN(colId)) {
3109
- console.error(pc.red(`Invalid collection ID: ${id}`));
3110
- process.exit(1);
3111
- }
3112
- await showCollectionDetail(client, colId);
3180
+ await showCollectionDetail(client, await resolveCollectionId(client, id));
3113
3181
  break;
3114
- }
3115
3182
  }
3116
3183
  } catch (err) {
3117
3184
  if (err instanceof MetabaseApiError) {
@@ -3454,10 +3521,10 @@ function buildReport(data) {
3454
3521
  report += `5. **Collection Structure** → Map collections to views (one view per business domain)\n`;
3455
3522
  report += `6. **Table Inventory** → Use field classification (dims/measures/time) to build each cube\n\n`;
3456
3523
  report += `Drill deeper with:\n`;
3457
- report += `- \`bon metabase explore table <id>\` — field types and classification\n`;
3458
- report += `- \`bon metabase explore card <id>\` — SQL and columns\n`;
3459
- report += `- \`bon metabase explore collection <id>\` — cards in a collection\n`;
3460
- report += `- \`bon metabase explore database <id>\` — schemas and tables\n\n`;
3524
+ report += `- \`bon metabase explore table <id-or-name>\` — field types and classification\n`;
3525
+ report += `- \`bon metabase explore card <id-or-name>\` — SQL and columns\n`;
3526
+ report += `- \`bon metabase explore collection <id-or-name>\` — cards in a collection\n`;
3527
+ report += `- \`bon metabase explore database <id-or-name>\` — schemas and tables\n\n`;
3461
3528
  report += `## Summary\n\n`;
3462
3529
  report += `| Metric | Count |\n|--------|-------|\n`;
3463
3530
  report += `| Databases | ${databases.length} |\n`;
@@ -3497,7 +3564,7 @@ function buildReport(data) {
3497
3564
  report += "```\n\n";
3498
3565
  report += `## Top ${topCards.length} Cards by Activity\n\n`;
3499
3566
  report += `Ranked by view count, weighted by recency. Cards not used in the last ${INACTIVE_MONTHS} months are penalized 90%.\n`;
3500
- report += `Use \`bon metabase explore card <id>\` to view SQL and column details for any card.\n\n`;
3567
+ report += `Use \`bon metabase explore card <id-or-name>\` to view SQL and column details for any card.\n\n`;
3501
3568
  report += `| Rank | ID | Views | Last Used | Active | Pattern | Type | Display | Collection | Name |\n`;
3502
3569
  report += `|------|----|-------|-----------|--------|---------|------|---------|------------|------|\n`;
3503
3570
  for (let i = 0; i < topCards.length; i++) {
@@ -3555,8 +3622,18 @@ function buildReport(data) {
3555
3622
  const sortedRefs = Array.from(globalTableRefs.entries()).sort((a, b) => b[1] - a[1]).slice(0, 20);
3556
3623
  report += `## Most Referenced Tables (from SQL)\n\n`;
3557
3624
  report += `Tables most frequently referenced in FROM/JOIN clauses across all cards.\n\n`;
3558
- report += `| Table | References |\n|-------|------------|\n`;
3559
- for (const [table, count] of sortedRefs) report += `| ${table} | ${count} |\n`;
3625
+ const tableIdByRef = /* @__PURE__ */ new Map();
3626
+ for (const db of databases) for (const t of db.tables) {
3627
+ const qualified = `${t.schema}.${t.name}`.toLowerCase();
3628
+ const unqualified = t.name.toLowerCase();
3629
+ if (!tableIdByRef.has(qualified)) tableIdByRef.set(qualified, t.id);
3630
+ if (!tableIdByRef.has(unqualified)) tableIdByRef.set(unqualified, t.id);
3631
+ }
3632
+ report += `| ID | Table | References |\n|------|-------|------------|\n`;
3633
+ for (const [table, count] of sortedRefs) {
3634
+ const tid = tableIdByRef.get(table);
3635
+ report += `| ${tid ?? "—"} | ${table} | ${count} |\n`;
3636
+ }
3560
3637
  report += `\n`;
3561
3638
  }
3562
3639
  const fieldIdLookup = /* @__PURE__ */ new Map();
@@ -3656,9 +3733,9 @@ function buildReport(data) {
3656
3733
  report += `### ${db.name} / ${schema} (${referenced.length} referenced`;
3657
3734
  if (unreferenced > 0) report += `, ${unreferenced} unreferenced`;
3658
3735
  report += `)\n\n`;
3659
- report += `| Table | Fields | Dims | Measures | Time | Refs |\n`;
3660
- report += `|-------|--------|------|----------|------|------|\n`;
3661
- for (const s of referenced) report += `| ${s.name} | ${s.fieldCount} | ${s.dimensions} | ${s.measures} | ${s.timeDimensions} | ${s.refCount} |\n`;
3736
+ report += `| ID | Table | Fields | Dims | Measures | Time | Refs |\n`;
3737
+ report += `|------|-------|--------|------|----------|------|------|\n`;
3738
+ for (const s of referenced) report += `| ${s.id} | ${s.name} | ${s.fieldCount} | ${s.dimensions} | ${s.measures} | ${s.timeDimensions} | ${s.refCount} |\n`;
3662
3739
  if (unreferenced > 0) skippedTables += unreferenced;
3663
3740
  report += `\n`;
3664
3741
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bonnard/cli",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "bon": "./dist/bin/bon.mjs"