@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.
- package/dist/bin/bon.mjs +142 -65
- 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("
|
|
3149
|
+
console.error(pc.red("Usage: bon metabase explore card <id-or-name>"));
|
|
3053
3150
|
process.exit(1);
|
|
3054
3151
|
}
|
|
3055
|
-
|
|
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("
|
|
3156
|
+
console.error(pc.red("Usage: bon metabase explore dashboard <id-or-name>"));
|
|
3066
3157
|
process.exit(1);
|
|
3067
3158
|
}
|
|
3068
|
-
|
|
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("
|
|
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,
|
|
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("
|
|
3170
|
+
console.error(pc.red("Usage: bon metabase explore table <id-or-name>"));
|
|
3092
3171
|
process.exit(1);
|
|
3093
3172
|
}
|
|
3094
|
-
|
|
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("
|
|
3177
|
+
console.error(pc.red("Usage: bon metabase explore collection <id-or-name>"));
|
|
3105
3178
|
process.exit(1);
|
|
3106
3179
|
}
|
|
3107
|
-
|
|
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
|
-
|
|
3559
|
-
for (const
|
|
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 +=
|
|
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
|
}
|